OCaml Programming: Correct + Efficient + Beautiful
OCaml 编程:正确+高效+美观 ¶

A textbook on functional programming and data structures in OCaml, with an emphasis on semantics and software engineering. This book is the textbook for CS 3110 Data Structures and Functional Programming at Cornell University. A past title of this book was “Functional Programming in OCaml”.
关于 OCaml 函数式编程和数据结构的教科书,重点是语义和软件工程。本书是康奈尔大学CS 3110数据结构和函数式编程的教科书。本书过去的标题是“OCaml 中的函数式编程”。

Spring 2023 Edition. 2023 年春季版。

Videos. There are over 200 YouTube videos embedded in this book. They can be watched independently of reading the book. Start with this YouTube playlist.
视频。本书中嵌入了 200 多个 YouTube 视频。它们可以独立于阅读本书而观看。从这个 YouTube 播放列表开始。

Authors. This book is based on courses taught by Michael R. Clarkson, Robert L. Constable, Nate Foster, Michael D. George, Dan Grossman, Daniel P. Huttenlocher, Dexter Kozen, Greg Morrisett, Andrew C. Myers, Radu Rugina, and Ramin Zabih. Together they have created over 20 years worth of course notes and intellectual contributions. Teasing out who contributed what is, by now, not an easy task. The primary compiler and author of this work in its form as a unified textbook is Michael R. Clarkson, who as of the Fall 2021 edition was the author of about 40% of the words and code tokens.
作者。本书基于 Michael R. Clarkson、Robert L. Constable、Nate Foster、Michael D. George、Dan Grossman、Daniel P. Huttenlocher、Dexter Kozen、Greg Morrisett、Andrew C. Myers、Radu Rugina 和 Ramin 教授的课程扎比赫。他们共同创造了 20 多年的课程笔记和智力贡献。到目前为止,弄清楚谁贡献了什么并不是一件容易的事。本书作为统一教科书的主要编译者和作者是 Michael R. Clarkson,截至 2021 年秋季版,他是大约 40% 的单词和代码标记的作者。

Copyright 2021–2023 Michael R. Clarkson. Released under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
版权所有 2021–2023 迈克尔·R·克拉克森。根据知识共享署名-非商业性-禁止衍生品 4.0 国际许可证发布。

Creative Commons License

About This Book 关于本书 ¶

Reporting Errors. If you find an error, please report it! Or if you have a suggestion about how to rewrite some part of the book, let us know. Just go to the page of the book for which you’d like to make a suggestion, click on the GitHub icon (it looks like a cat) near the top right of the page, and click “open issue” or “suggest edit”. The latter is a little heavier weight, because it requires you to fork the textbook repository with GitHub. But for minor edits that will be appreciated and lead to much quicker uptake of suggestions.
报告错误。如果您发现错误,请报告!或者,如果您对如何重写本书的某些部分有建议,请告诉我们。只需转到您想要提出建议的书籍页面,单击页面右上角附近的 GitHub 图标(看起来像一只猫),然后单击“打开问题”或“建议编辑” 。后者的重量稍重一些,因为它需要你使用 GitHub 来 fork 教科书存储库。但对于较小的编辑,我们将不胜感激,并导致更快地采纳建议。

Background. This book is used at Cornell for a third-semester programming course. Most students have had one semester of introductory programming in Python, followed by one semester of object-oriented programming in Java. Frequent comparisons are therefore made to those two languages. Readers who have studied similar languages should have no difficulty following along. The book does not assume any prior knowledge of functional programming, but it does assume that readers have prior experience programming in some mainstream imperative language. Knowledge of discrete mathematics at the level of a standard first-semester CS course is also assumed.
背景。这本书在康奈尔大学第三学期的编程课程中使用。大多数学生已经学习了一学期的 Python 入门编程,然后学习了一学期的 Java 面向对象编程。因此,人们经常对这两种语言进行比较。学习过类似语言的读者应该不会有困难。本书不假设读者有任何函数式编程的先验知识,但它确实假设读者有一些主流命令式语言的编程经验。还假定您具备标准第一学期计算机科学课程水平的离散数学知识。

Videos. You will find over 200 YouTube videos embedded throughout this book. The videos usually provide an introduction to material, upon which the textbook then expands. These videos were produced during pandemic when the Cornell course that uses this textbook, CS 3110, had to be asynchronous. The student response to them was overwhelmingly positive, so they are now being made public as part of the textbook. But just so you know, they were not produced by a professional A/V team—just a guy in his basement who was learning as he went.
视频。本书中嵌入了 200 多个 YouTube 视频。这些视频通常提供材料介绍,然后教科书在此基础上进行扩展。这些视频是在大流行期间制作的,当时使用这本教科书 CS 3110 的康奈尔大学课程必须是异步的。学生对它们的反应非常积极,因此它们现在作为教科书的一部分公开。但你要知道,它们并不是由专业的 A/V 团队制作的,而是由一个在地下室里一边学习一边制作的人制作的。

The videos mostly use the versions of OCaml and its ecosystem that were current in Fall 2020. Current versions you are using are likely to look different from the videos, but don’t be alarmed: the underlying ideas are the same. The most visible difference is likely to be the VS Code plugin for OCaml. In Fall 2020 the badly-aging “OCaml and Reason IDE” plugin was still being used. It has since been superseded by the “OCaml Platform” plugin.
这些视频主要使用 2020 年秋季发布的 OCaml 及其生态系统版本。您使用的当前版本可能与视频有所不同,但不要惊慌:基本思想是相同的。最明显的区别可能是 OCaml 的 VS Code 插件。 2020 年秋季,老化严重的“OCaml 和 Reason IDE”插件仍在使用。此后它已被“OCaml Platform”插件取代。

The order that the textbook covers topics sometimes differs from the order that the videos cover the topics, simply because the videos originate from lectures. The videos are placed in the textbook nearest to the topic they cover, but that does mean sometimes the videos are not in chronological order. To watch them in their original order, start with this YouTube playlist.
教科书涵盖主题的顺序有时与视频涵盖主题的顺序不同,这仅仅是因为视频源自讲座。这些视频放置在最接近其所涵盖主题的教科书中,但这确实意味着有时视频不按时间顺序排列。要按原始顺序观看它们,请从这个 YouTube 播放列表开始。

Collaborative Annotations. At the right margin of each page, you will find an annotation feature provided by hypothes.is. You can use this to highlight and make private notes as you study the text. You can form study groups to share your annotations, or share them publicly. Check out these tips for how to annotate effectively.
协作注释。在每页的右侧边缘,您会发现由hypothes.is提供的注释功能。您可以在学习文本时使用它来突出显示并做私人笔记。您可以组建学习小组来共享您的注释,或公开共享它们。查看这些技巧,了解如何有效地进行注释。

Executable Code. Many pages of this book have OCaml code embedded in them. The output of that code is already shown in the book. Here’s an example:
可执行代码。本书的许多页面都嵌入了 OCaml 代码。该代码的输出已在书中显示。这是一个例子:

print_endline "Hello world!"
Hello world!
- : unit = ()

You can also edit and re-run the code yourself to experiment and check your understanding. Look for the icon near the top right of the page that looks like a rocket ship. In the drop-down menu you’ll find two ways to interact with the code:
您还可以自己编辑并重新运行代码来实验并检查您的理解。寻找页面右上角附近看起来像火箭飞船的图标。在下拉菜单中,您将找到两种与代码交互的方式:

  • Binder will launch the site mybinder.org, which is a free cloud-based service for “reproducible, interactive, shareable environments for science at scale.” All the computation happens in their cloud servers, but the UI is provided through your browser. It will take a little while for the textbook page to open in Binder. Once it does, you can edit and run the code in a Jupyter notebook. Jupyter notebooks are documents (usually ending in the .ipynb extension) that can be viewed in web browsers and used to write narrative content as well as code. They became popular in data science communities (especially Python, R, and Julia) as a way of sharing analyses. Now many languages can run in Jupyter notebooks, including OCaml. Code and text are written in cells in a Jupyter notebook. Look at the “Cell” menu in it for commands to run cells. Note that Shift-Enter is usually a hotkey for running the cell that has focus.
    Binder 将推出 mybinder.org 网站,这是一项基于云的免费服务,旨在为“大规模科学提供可重复、交互式、可共享的环境”。所有计算都发生在他们的云服务器中,但用户界面是通过浏览器提供的。教科书页面需要一段时间才能在 Binder 中打开。一旦完成,您可以在 Jupyter 笔记本中编辑和运行代码。 Jupyter 笔记本是可以在 Web 浏览器中查看并用于编写叙述性内容和代码的文档(通常以 .ipynb 扩展名结尾)。它们作为共享分析的一种方式在数据科学社区(尤其是 Python、R 和 Julia)中变得流行。现在许多语言都可以在 Jupyter Notebook 中运行,包括 OCaml。代码和文本写入 Jupyter 笔记本的单元格中。查看其中的“单元格”菜单以获取运行单元格的命令。请注意,Shift-Enter 通常是运行具有焦点的单元格的热键。

  • Live code will actually do about the same thing, except that instead of leaving the current textbook page and taking you off to Binder, it will modify the code cells on the page to be editable. It takes some time for the connection to be made behind the scenes, during which you will see “Waiting for kernel”. After the connection has been made, you can edit all the code cells on the page and re-run them. Live code is temporarily disabled in this textbook due to a bug in Jupyter Book. As a workaround, use Binder as described above.
    实时代码实际上会做同样的事情,只不过它不会离开当前的教科书页面并将您带到 Binder,而是会将页面上的代码单元修改为可编辑。在后台建立连接需要一些时间,在此期间您将看到“等待内核”。建立连接后,您可以编辑页面上的所有代码单元格并重新运行它们。由于 Jupyter Book 中的错误,本教科书中暂时禁用了实时代码。作为解决方法,请如上所述使用 Binder。

Try interacting with the cell above now to make it print a string of your choice. How about: "Camels are bae."
现在尝试与上面的单元格交互,使其打印您选择的字符串。怎么样: "Camels are bae."

Tip

When you write “real” OCaml code, this is not the interface you’ll be using. You’ll write code in an editor such as Visual Studio Code or Emacs, and you’ll compile it from a terminal. Binder and Live Code are just for interacting seamlessly with the textbook.
当您编写“真正的”OCaml 代码时,这不是您将使用的界面。您将在 Visual Studio Code 或 Emacs 等编辑器中编写代码,然后从终端进行编译。 Binder 和 Live Code 只是为了与教科书无缝交互。

Downloadable Pages. Each page of this book is downloadable in a variety of formats. The download icon is at the top right of each page. You’ll always find the original source code of the page, which is usually Markdown—or more precisely MyST Markdown, which is an extension of Markdown for technical writing. Each page is also individually available as PDF, which simply prints from your browser. For the entire book as a PDF, see the paragraph about that below.
可下载的页面。本书的每一页都可以多种格式下载。下载图标位于每个页面的右上角。你总能找到页面的原始源代码,通常是 Markdown,或更准确地说是 MyST Markdown,它是 Markdown 用于技术写作的扩展。每个页面还可以单独提供 PDF 格式,只需从浏览器中打印即可。对于 PDF 格式的整本书,请参阅下面的段落。

Pages with OCaml code cells embedded in them can also be downloaded as Jupyter notebooks. To run those locally on your own machine (instead of in the cloud on Binder), you’ll need to install Jupyter. The easiest way of doing that is typically to install Anaconda. Then you’ll need to install OCaml Jupyter, which requires that you already have OCaml installed. To be clear, there’s no need to install Jupyter or to use notebooks. It’s just another way to interact with this textbook beyond reading it.
嵌入 OCaml 代码单元的页面也可以作为 Jupyter 笔记本下载。要在您自己的计算机上本地运行这些程序(而不是在 Binder 上的云中),您需要安装 Jupyter。最简单的方法通常是安装 Anaconda。然后您需要安装 OCaml Jupyter,这要求您已经安装了 OCaml。需要明确的是,无需安装 Jupyter 或使用笔记本。这只是阅读之外与这本教科书互动的另一种方式。

Exercises and Solutions. At the end of each chapter except the first, you will find a section of exercises. The exercises are annotated with a difficulty rating:
练习和解决方案。除第一章外,每章末尾都有练习部分。这些练习都标有难度等级:

  • One star [★]: easy exercises that should take only a minute or two.
    一颗星 [★]:简单的练习,只需一两分钟。

  • Two stars [★★]: straightforward exercises that should take a few minutes.
    两颗星[★★]:简单的练习,应该需要几分钟。

  • Three stars [★★★]: exercises that might require anywhere from five to twenty minutes or so.
    三颗星[★★★]:练习可能需要五到二十分钟左右。

  • Four [★★★★] or more stars: challenging or time-consuming exercises provided for students who want to dig deeper into the material.
    四颗 [★★★★] 或更多星:为想要深入研究材料的学生提供具有挑战性或耗时的练习。

It’s possible we’ve misjudged the difficulty of a problem from time to time. Let us know if you think an annotation is off.
我们有时可能会误判问题的难度。如果您认为注释已关闭,请告诉我们。

Please do not post your solutions to the exercises anywhere, especially not in public repositories where they could be found by search engines. Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
请不要将您的练习解决方案发布在任何地方,尤其是不要发布在可以被搜索引擎找到的公共存储库中。大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。

PDF. A full PDF version of this book is available. It does not contain the embedded videos, annotations, or other features that the HTML version has. It might also have typesetting errors. At this time, no tablet (ePub, etc.) version is available, but most tablets will let you import PDFs.
PDF。本书有完整的 PDF 版本。它不包含 HTML 版本所具有的嵌入视频、注释或其他功能。它还可能存在排版错误。目前,没有可用的平板电脑(ePub 等)版本,但大多数平板电脑都可以让您导入 PDF。

Installing OCaml 安装 OCaml ¶

If all you need is a way to follow along with the code examples in this book, you don’t actually have to install OCaml! The code on each page is executable in your browser, as described earlier in this Preface.
如果您需要的只是一种遵循本书中的代码示例的方法,那么您实际上不必安装 OCaml!每个页面上的代码都可以在浏览器中执行,如本前言前面所述。

If you want to take it a step further but aren’t ready to spend time installing OCaml yourself, we provide a virtual machine with OCaml pre-installed inside a Linux OS.
如果您想更进一步,但又不准备花时间自己安装 OCaml,我们提供了一个在 Linux 操作系统中预装了 OCaml 的虚拟机。

But if you want to do OCaml development on your own, you’ll need to install it on your machine. There’s no universally “right” way to do that. The instructions below are for Cornell’s CS 3110 course, which has goals and needs beyond just OCaml. Nonetheless, you might find them to be useful even if you’re not a student in the course.
但如果您想自己进行 OCaml 开发,则需要将其安装在您的计算机上。没有普遍“正确”的方法可以做到这一点。以下说明适用于康奈尔大学的 CS 3110 课程,该课程的目标和需求不仅仅是 OCaml。尽管如此,即使您不是该课程的学生,您也可能会发现它们很有用。

Here’s what we’re going to install:
这是我们要安装的内容:

  • A Unix development environment
    Unix 开发环境

  • OPAM, the OCaml Package Manager
    OPAM,OCaml 包管理器

  • An OPAM  一个OPAMswitch 转变 with the OCaml compiler and some packages
    使用 OCaml 编译器和一些包

  • The Visual Studio Code editor, with OCaml support
    Visual Studio Code 编辑器,支持 OCaml

The installation process will rely heavily on the terminal, or text interface to your computer. If you’re not too familiar with it, you might want to brush up with a terminal tutorial.
安装过程将严重依赖于计算机的终端或文本界面。如果您不太熟悉它,您可能需要温习一下终端教程。

Tip

If this is your first time installing development software, it’s worth pointing out that “close doesn’t count”: trying to proceed past an error usually just leads to worse errors, and sadness. That’s because we’re installing a kind of tower of software, with each level of the tower building on the previous. If you’re not building on a solid foundation, the whole thing might collapse. The good news is that if you do get an error, you’re probably not alone. A quick google search will often turn up solutions that others have discovered. Of course, do think critically about suggestions made by random strangers on the internet.
如果这是您第一次安装开发软件,那么值得指出的是“关闭不算数”:尝试克服错误通常只会导致更严重的错误和悲伤。这是因为我们正在安装一种软件塔,塔的每一层都建立在前一层之上。如果你没有建立在坚实的基础上,整个事情可能会崩溃。好消息是,如果您确实遇到错误,您可能并不孤单。快速谷歌搜索通常会找到其他人已经发现的解决方案。当然,一定要批判性地思考互联网上随机陌生人提出的建议。

Let’s get started! 让我们开始吧!

Unix Development Environment
Unix 开发环境 ¶

Important 重要的

First, upgrade your OS. If you’ve been intending to make any major OS upgrades, do them now. Otherwise when you do get around to upgrading, you might have to repeat some or all of this installation process. Better to get it out of the way beforehand.
首先,升级您的操作系统。如果您打算对操作系统进行任何重大升级,请立即进行。否则,当您确实要升级时,您可能必须重复部分或全部安装过程。最好提前把它弄走。

Linux Linux ¶

If you’re already running Linux, you’re done with this step. Proceed to Install OPAM, below.
如果您已经在运行 Linux,则此步骤已完成。继续安装下面的 OPAM。

Mac 苹果 ¶

Beneath the surface, macOS is already a Unix-based OS. But you’re going to need some developer tools and a Unix package manager. There are two to pick from: Homebrew and MacPorts. From the perspective of this textbook and CS 3110, it doesn’t matter which you choose:
从表面上看,macOS 已经是一个基于 Unix 的操作系统。但您将需要一些开发人员工具和 Unix 包管理器。有两个可供选择:Homebrew 和 MacPorts。从本教材和 CS 3110 的角度来看,您选择哪一个并不重要:

  • If you’re already accustomed to one, feel free to keep using it. Make sure to run its update command before continuing with these instructions.
    如果您已经习惯了,请随意继续使用它。请确保在继续执行这些说明之前运行其更新命令。

  • Otherwise, pick one and follow the installation instructions on its website. The installation process for Homebrew is typically easier and faster, which might nudge you in that direction. If you do choose MacPorts, make sure to follow all the detailed instructions on its page, including XCode and an X11 server. Do not install both Homebrew and MacPorts; they aren’t meant to co-exist. If you change your mind later, make sure to uninstall one before installing the other.
    否则,请选择一个并按照其网站上的安装说明进行操作。 Homebrew 的安装过程通常更容易、更快,这可能会推动您朝这个方向发展。如果您选择 MacPorts,请确保遵循其页面上的所有详细说明,包括 XCode 和 X11 服务器。不要同时安装 Homebrew 和 MacPorts;它们不应该共存。如果您稍后改变主意,请务必先卸载其中一个,然后再安装另一个。

After you’ve finished installing/updating either Homebrew or MacPorts, proceed to Install OPAM, below.
完成 Homebrew 或 MacPorts 的安装/更新后,请继续安装下面的 OPAM。

Windows 视窗 ¶

Unix development in Windows is made possible by the Windows Subsystem for Linux (WSL). If you have a recent version of Windows (build 20262, released November 2020, or newer), WSL is easy to install. If you don’t have that recent of a version, try running Windows Update to get it.
Windows 中的 Unix 开发是通过 Windows Linux 子系统 (WSL) 实现的。如果您有最新版本的 Windows(内部版本 20262、2020 年 11 月发布或更高版本),WSL 很容易安装。如果您没有最新版本,请尝试运行 Windows 更新来获取它。

Tip

If you get an error about the “virtual machine” while installing WSL, you might need to enable virtualization in your machine’s BIOS. The instructions for that are dependent on the manufacturer of your machine. Try googling “enable virtualization [manufacturer] [model]”, substituting for the manufacturer and model of your machine. This Red Hat Linux page might also help.
如果您在安装 WSL 时收到有关“虚拟机”的错误,您可能需要在计算机的 BIOS 中启用虚拟化。相关说明取决于您的机器的制造商。尝试在谷歌上搜索“启用虚拟化[制造商][型号]”,替换您机器的制造商和型号。此 Red Hat Linux 页面也可能有所帮助。

With a recent version of Windows, and assuming you’ve never installed WSL before, here’s all you have to do:
使用最新版本的 Windows,并且假设您以前从未安装过 WSL,那么您只需执行以下操作:

  • Open Windows PowerShell as Administrator. To do that, click Start, type PowerShell, and it should come up as the best match. Click “Run as Administrator”, and click Yes to allow changes.
    以管理员身份打开 Windows PowerShell。为此,请单击“开始”,键入 PowerShell,它应该会显示为最佳匹配。单击“以管理员身份运行”,然后单击“是”以允许更改。

  • Run wsl --install. (Or, if you have already installed WSL but not Ubuntu before, then instead run wsl --install -d Ubuntu.) When the Ubuntu download is completed, it will likely ask you to reboot. Do so. The installation will automatically resume after the reboot.
    运行 wsl --install 。 (或者,如果您之前已经安装了 WSL 但没有安装 Ubuntu,则运行 wsl --install -d Ubuntu 。)当 Ubuntu 下载完成后,它可能会要求您重新启动。这样做。重新启动后将自动恢复安装。

  • You will be prompted to create a Unix username and password. You can use any username and password you wish. It has no bearing on your Windows username and password (though you are free to re-use those). Do not put a space in your username. Do not forget your password. You will need it in the future.
    系统将提示您创建 Unix 用户名和密码。您可以使用任何您想要的用户名和密码。它与您的 Windows 用户名和密码无关(尽管您可以自由地重复使用它们)。请勿在您的用户名中添加空格。不要忘记您的密码。你将来会需要它。

Warning 警告

Do not proceed with these instructions if you were not prompted to create a Unix username and password. Something has gone wrong. Perhaps your Ubuntu installation did not complete correctly. Try uninstalling Ubuntu and reinstalling it through the Windows Start menu.
如果系统未提示您创建 Unix 用户名和密码,请不要继续执行这些说明。出了问题。也许您的 Ubuntu 安装没有正确完成。尝试卸载 Ubuntu 并通过 Windows 开始菜单重新安装。

Now skip to the “Ubuntu setup” paragraph below.
现在跳到下面的“Ubuntu 设置”段落。

Without a recent version of Windows, you will need to follow Microsoft’s manual install instructions. WSL2 is preferred over WSL1 by OCaml (and WSL2 offers performance and functionality improvements), so install WSL2 if you can.
如果没有最新版本的 Windows,您将需要遵循 Microsoft 的手动安装说明。 OCaml 更倾向于 WSL2 而非 WSL1(并且 WSL2 提供性能和功能改进),因此如果可以的话请安装 WSL2。

Ubuntu setup. These rest of these instructions assume that you installed Ubuntu (20.04) as the Linux distribution. That is the default distribution in WSL. In principle other distributions should work, but might require different commands from this point forward.
Ubuntu 设置。这些说明的其余部分假设您安装了 Ubuntu (20.04) 作为 Linux 发行版。这是 WSL 中的默认分布。原则上其他发行版应该可以工作,但从现在开始可能需要不同的命令。

Open the Ubuntu app. (It might already be open if you just finished installing WSL.) You will be at the Bash prompt, which looks something like this:
打开 Ubuntu 应用程序。 (如果您刚刚安装完 WSL,它可能已经打开。)您将看到 Bash 提示符,如下所示:

user@machine:~$

Warning 警告

If that prompt instead looks like root@...#, something is wrong. Did you create a Unix username and password for Ubuntu in the earlier step above? If so, the username in this prompt should be the username you chose back then, not root. Do not proceed with these instructions if your prompt looks like root@...#. Perhaps you could uninstall Ubuntu and reinstall it.
如果该提示看起来像 root@...# ,则说明有问题。您是否在前面的步骤中为 Ubuntu 创建了 Unix 用户名和密码?如果是这样,则此提示中的用户名应该是您当时选择的用户名,而不是 root 。如果您的提示类似于 root@...# ,请不要继续执行这些说明。也许您可以卸载 Ubuntu 并重新安装。

Enable copy-and-paste: 启用复制粘贴:

  • Click on the Ubuntu icon on the top left of the window.
    单击窗口左上角的 Ubuntu 图标。

  • Click Properties 单击属性

  • Make sure “Use Ctrl+Shift+C/V as Copy/Paste” is checked.
    确保选中“使用 Ctrl+Shift+C/V 作为复制/粘贴”。

Now Ctrl+Shift+C will copy and Ctrl+Shift+V will paste into the terminal. Note that you have to include Shift as part of that keystroke.
现在 Ctrl+Shift+C 将复制并 Ctrl+Shift+V 将粘贴到终端中。请注意,您必须将 Shift 包含在该击键中。

Run the following command to update the APT package manager, which is what helps to install Unix packages:
运行以下命令来更新 APT 包管理器,这有助于安装 Unix 包:

sudo apt update

You will be prompted for the Unix password you chose. The prefix sudo means to run the command as the administrator, aka “super user”. In other words, do this command as super user, hence, “sudo”.
系统将提示您输入您选择的 Unix 密码。前缀 sudo 表示以管理员(又称“超级用户”)身份运行命令。换句话说,以超级用户身份执行此命令,即“sudo”。

Warning 警告

Running commands with sudo is potentially dangerous and should not be done lightly. Do not get into the habit of putting sudo in front of commands, and do not randomly try it without reason.
使用 sudo 运行命令有潜在危险,不应轻易执行。不要养成在命令前面加上 sudo 的习惯,也不要无缘无故地随意尝试。

Now run this command to upgrade all the APT software packages:
现在运行以下命令来升级所有APT软件包:

sudo apt upgrade -y

Then install some useful packages that we will need:
然后安装我们需要的一些有用的包:

sudo apt install -y zip unzip build-essential

File Systems. WSL has its own filesystem that is distinct from the Windows file system, though there are ways to access each from the other.
文件系统。 WSL 有自己的文件系统,该文件系统与 Windows 文件系统不同,尽管有多种方法可以相互访问。

  • When you launch Ubuntu and get the $ prompt, you are in the WSL file system. Your home directory there is named ~, which is a built-in alias for /home/your_ubuntu_user_name. You can run explorer.exe . (note the dot at the end of that) to open your Ubuntu home directory in Windows explorer.
    当您启动 Ubuntu 并出现 $ 提示符时,您就处于 WSL 文件系统中。您的主目录名为 ~ ,它是 /home/your_ubuntu_user_name 的内置别名。您可以运行 explorer.exe . (注意其末尾的点)在Windows资源管理器中打开您的Ubuntu主目录。

  • From Ubuntu, you can access your Windows home directory at the path /mnt/c/Users/your_windows_user_name/.
    在 Ubuntu 中,您可以通过路径 /mnt/c/Users/your_windows_user_name/ 访问 Windows 主目录。

  • From Windows Explorer, you can access your Ubuntu home directory under the Linux icon in the left-hand list (near “This PC” and “Network”), then navigating to Ubuntu → homeyour_ubuntu_user_name. Or you can go there directly by typing into the Windows Explorer path bar: \\wsl$\Ubuntu\home\your_ubuntu_user_name.
    从 Windows 资源管理器中,您可以访问左侧列表中 Linux 图标下的 Ubuntu 主目录(靠近“此电脑”和“网络”),然后导航到 Ubuntu → homeyour_ubuntu_user_name 直接转到那里。

Practice accessing your Ubuntu and Windows home directories now, and make sure you can recognize which you are in. For advanced information, see Microsoft’s guide to Windows and Linux file systems.
现在练习访问您的 Ubuntu 和 Windows 主目录,并确保您可以识别您所在的目录。有关高级信息,请参阅 Microsoft 的 Windows 和 Linux 文件系统指南。

We recommend storing your OCaml development work in your Ubuntu home directory, not your Windows home directory. By implication, Microsoft also recommends that in the guide just linked.
我们建议将 OCaml 开发工作存储在 Ubuntu 主目录中,而不是 Windows 主目录中。言下之意,微软还建议在刚刚链接的指南中这样做。

Install OPAM 安装 OPAM ¶

Linux. Follow the instructions for your distribution.
Linux。请遵循您的发行版的说明。

Mac. If you’re using Homebrew, run this command:
苹果。如果您使用的是 Homebrew,请运行以下命令:

brew install opam

If you’re using MacPorts, run this command:
如果您使用的是 MacPorts,请运行以下命令:

sudo port install opam

Windows. Run this command from Ubuntu:
视窗。从 Ubuntu 运行此命令:

sudo apt install opam

Initialize OPAM 初始化 OPAM ¶

Warning 警告

Do not put sudo in front of any opam commands. That would break your OCaml installation.
请勿将 sudo 放在任何 opam 命令前面。这会破坏你的 OCaml 安装。

Linux, Mac, and WSL2. Run:
Linux、Mac 和 WSL2。跑步:

opam init --bare -a -y

It is expected behavior to get a note about making sure .profile is well sourced in .bashrc. You don’t need to do anything about that.
预期行为会得到有关确保 .profile.bashrc 中来源良好的注释。你不需要对此做任何事情。

WSL1. Hopefully you are running WSL2, not WSL1. But on WSL1, run:
WSL1。希望您运行的是 WSL2,而不是 WSL1。但在 WSL1 上运行:

opam init --bare -a -y --disable-sandboxing

It is necessary to disable sandboxing because of an issue involving OPAM and WSL1.
由于涉及 OPAM 和 WSL1 的问题,有必要禁用沙箱。

Create an OPAM Switch
创建 OPAM 开关 ¶

A switch 转变 is a named installation of OCaml with a particular compiler version and set of packages.
是 OCaml 的命名安装,具有特定的编译器版本和软件包集。
You can have many switches and, well, switch between them —whence the name. Create a switch for this semester’s CS 3110 by running this command:

opam switch create cs3110-2023sp ocaml-base-compiler.4.14.0

Tip

If that command fails saying that the 4.14.0 compiler can’t be found, you probably installed OPAM sometime back in the past and now need to update it. Do so with opam update.
如果该命令失败并提示无法找到 4.14.0 编译器,则您可能在过去安装过 OPAM,现在需要更新它。使用 opam update 执行此操作。

You might be prompted to run the next command. It won’t matter whether you do or not, because of the very next step we’re going to do (i.e., logging out).
系统可能会提示您运行下一个命令。无论您是否这样做都没关系,因为我们接下来要做的就是(即注销)。

eval $(opam env)

Now we need to make sure your OCaml environment was configured correctly. Logout from your OS (or just reboot). Then re-open your terminal and run this command:
现在我们需要确保您的 OCaml 环境配置正确。从操作系统注销(或重新启动)。然后重新打开终端并运行以下命令:

opam switch list

You should get output like this:
您应该得到如下输出:

#  switch         compiler                    description
→  cs3110-2023sp  ocaml-base-compiler.4.14.0  cs3110-2023sp

There might be other lines if you happen to have done OCaml development before. Here’s what to check for:
如果您之前做过 OCaml 开发,可能还有其他行。以下是要检查的内容:

  • You must not get a warning that “The environment is not in sync with the current switch. You should run eval $(opam env)”. If either of the two issues below also occur, you need to resolve this issue first.
    您不得收到“环境与当前交换机不同步”的警告。你应该运行 eval $(opam env) ”。如果同时出现以下两个问题,则需要先解决该问题。

  • There must be a right arrow in the first column next to the cs3110-2023sp switch.
    cs3110-2023sp 开关旁边的第一列中必须有一个向右箭头。

  • That switch must have the right name and the right compiler version, 4.14.0.
    该开关必须具有正确的名称和正确的编译器版本,4.14.0。

Warning 警告

If you do get that warning about opam env, something is wrong. Your shell is probably not running the OPAM configuration commands that opam init was meant to install. You could try opam init --reinit to see whether that fixes it. Also, make sure you really did log out of your OS (or reboot).
如果您确实收到有关 opam env 的警告,则说明有问题。您的 shell 可能没有运行 opam init 本来要安装的 OPAM 配置命令。您可以尝试 opam init --reinit 看看是否可以解决问题。另外,请确保您确实注销了操作系统(或重新启动)。

Continue by installing the OPAM packages we need:
继续安装我们需要的 OPAM 包:

opam install -y utop odoc ounit2 qcheck bisect_ppx menhir ocaml-lsp-server ocamlformat ocamlformat-rpc

Make sure to grab that whole line above when you copy it. You will get some output about editor configuration. Unless you intend to use Emacs or Vim for OCaml development, you can safely ignore that output. We’re going to use VS Code as the editor in these instructions, so let’s ignore it.
复制时请务必抓住上面的整行。您将获得一些有关编辑器配置的输出。除非您打算使用 Emacs 或 Vim 进行 OCaml 开发,否则您可以安全地忽略该输出。在这些说明中,我们将使用 VS Code 作为编辑器,所以我们忽略它。

You should now be able to launch utop, the OCaml Universal Toplevel.
您现在应该能够启动 utop,OCaml 通用顶级。

utop

Tip

You should see a message “Welcome to utop version … (using OCaml version 4.14.0)!” If the OCaml version is incorrect, then you probably have an environment issue. See the tip above about the opam env command.
您应该看到一条消息“欢迎使用 utop 版本...(使用 OCaml 版本 4.14.0)!”如果 OCaml 版本不正确,则可能存在环境问题。请参阅上面有关 opam env 命令的提示。

Enter 3110 followed by two semi-colons. Press return. The # is the utop prompt; you do not type it yourself.
输入 3110,后跟两个分号。按回车键。 # 是 utop 提示符;您不自己输入。

# 3110;;
- : int = 3110

Stop to appreciate how lovely 3110 is. Then quit utop. Note that this time you must enter the extra # before the quit directive.
停下来欣赏 3110 是多么可爱。然后退出utop。请注意,这次您必须在 quit 指令之前输入额外的 #。

# #quit;;

A faster way to quit is to type Control+D.
更快的退出方法是按 Control+D。

Double Check OCaml 仔细检查 OCaml ¶

If you’re having any trouble with your installation, follow these double-check instructions. Some of them repeat the tips we provided above, but we’ve put them all here in one place to help diagnose any issues.
如果您在安装时遇到任何问题,请按照这些仔细检查说明进行操作。其中一些重复了我们上面提供的提示,但我们将它们全部放在一处以帮助诊断任何问题。

First, reboot your computer. We need a clean slate for this double check.
首先,重新启动计算机。我们需要一个干净的记录来进行这次双重检查。

Second, run utop, and make sure it works. If it does not, here are some common issues:
其次,运行 utop,并确保其正常工作。如果没有,以下是一些常见问题:

  • Are you in the right Unix prompt? On Mac, make sure you are in whatever Unix shell is the default for your Terminal: don’t run bash or zsh or anything else manually to change the shell. On Windows, make sure you are in the Ubuntu app, not PowerShell or Cmd.
    您处于正确的 Unix 提示符中吗?在 Mac 上,确保您使用的是终端默认的 Unix shell:不要手动运行 bash 或 zsh 或其他任何内容来更改 shell。在 Windows 上,确保您使用的是 Ubuntu 应用程序,而不是 PowerShell 或 Cmd。

  • Is the OPAM environment set? If utop isn’t a recognized command, run eval $(opam env) then try running utop again. If utop now works, your login shell is somehow not running the right commands to automatically activate the OPAM environment; you shouldn’t have to manually activate the environment with the eval command. Probably something went wrong earlier when you ran the opam init command. To fix it, follow the “redo” instructions below.
    OPAM环境设置了吗?如果 utop 不是可识别的命令,请运行 eval $(opam env) ,然后再次尝试运行 utop。如果 utop 现在可以工作,则说明您的登录 shell 未运行正确的命令来自动激活 OPAM 环境;您不必使用 eval 命令手动激活环境。当您运行 opam init 命令时,可能之前出现了问题。要修复它,请按照下面的“重做”说明进行操作。

  • Is your switch listed? Run opam switch list and make sure a switch named cs3110-2023sp is listed, that it has the 4.14.0 compiler, and that it is the active switch (which is indicated with an arrow beside it). If that switch is present but not active, run opam switch cs3110-2023sp then see whether utop works. If that switch is not present, follow the “redo” instructions below.
    您的交换机已列出吗?运行 opam switch list 并确保列出名为 cs3110-2023sp 的开关,它具有 4.14.0 编译器,并且它是活动开关(旁边用箭头指示) 。如果该开关存在但未激活,请运行 opam switch cs3110-2023sp 然后查看 utop 是否工作。如果该开关不存在,请按照下面的“重做”说明进行操作。

Redo Instructions: Remove the OPAM directory by running rm -r ~/.opam. Then go back to the OPAM initialization step in the instructions way above, and proceed forward. Be extra careful to use the exact OPAM commands given above; sometimes mistakes occur when parts of them are omitted. Finally, redo the double check: reboot and see whether utop still works.
重做说明:通过运行 rm -r ~/.opam 删除 OPAM 目录。然后按照上面的说明回到OPAM初始化步骤,然后继续。使用上面给出的确切 OPAM 命令时要格外小心;有时,由于省略了部分内容,就会出现错误。最后,重做双重检查:重新启动并查看 utop 是否仍然有效。

Important 重要的

You want to get to the point where utop immediately works after a reboot, without having to type any additional commands.
您希望 utop 在重新启动后立即工作,而无需键入任何其他命令。

Visual Studio Code Visual Studio Code ¶

Visual Studio Code is a great choice as a code editor for OCaml. (Though if you are already a power user of Emacs or Vim those are great, too.)
Visual Studio Code 是 OCaml 代码编辑器的绝佳选择。 (不过,如果您已经是 Emacs 或 Vim 的高级用户,那么这些也很棒。)

First, download and install Visual Studio Code (henceforth, VS Code). Launch VS Code. Open the extensions pane, either by going to View → Extensions, or by clicking on the icon for it in the column of icons on the left — it looks like four little squares, the top-right of which is separated from the other three.
首先,下载并安装 Visual Studio Code(以下简称 VS Code)。启动 VS 代码。打开扩展窗格,可以转到“视图”→“扩展”,或者单击左侧图标栏中的图标 - 它看起来像四个小方块,右上角与其他三个小方块分开。

Second, follow one of these steps if you are on Windows or Mac:
其次,如果您使用的是 Windows 或 Mac,请执行以下步骤之一:

  • Windows only: Install the “WSL” extension. Second, open a WSL window by using the command “WSL: New WSL Window”. The first time you do this, it will install some additional software. After that completes, you will see a green “WSL: Ubuntu” indicator in the bottom-left of the VS Code window. Make sure that you see “WSL: Ubuntu” there before proceeding with the instructions below. If you see just an icon that looks like >< then click it, and choose “New WSL Window” from the Command Palette that opens.
    仅限 Windows:安装“WSL”扩展。其次,使用命令“WSL: New WSL Window”打开 WSL 窗口。第一次执行此操作时,它将安装一些附加软件。完成后,您将在 VS Code 窗口左下角看到一个绿色的“WSL:Ubuntu”指示器。在继续执行以下说明之前,请确保您在那里看到“WSL:Ubuntu”。如果您只看到一个类似 > < 的图标,请单击它,然后从打开的命令面板中选择“新建 WSL 窗口”。

  • Mac only: Open the Command Palette and type “shell command” to find the “Shell Command: Install ‘code’ command in PATH” command. Run it.
    仅限 Mac:打开命令面板并输入“shell 命令”以查找“Shell 命令:在 PATH 中安装‘code’命令”命令。运行。

Third, regardless of your OS, close any open terminals — or just logout or reboot — to let the new path settings take effect, so that you will later be able to launch VS Code from the terminal.
第三,无论您使用哪种操作系统,请关闭所有打开的终端(或者只是注销或重新启动)以使新路径设置生效,以便您稍后能够从终端启动 VS Code。

Fourth, again open the VS Code extensions pane. Search for and install the “OCaml Platform” extension from OCaml Labs. Be careful to install the extension with exactly that name. (If you happen to note a “build failing” icon on the extension’s page, don’t be concerned.)
第四,再次打开 VS Code 扩展窗格。从 OCaml Labs 搜索并安装“OCaml Platform”扩展。请小心安装具有完全相同名称的扩展。 (如果您碰巧在扩展程序页面上注意到“构建失败”图标,请不要担心。)

Warning 警告

The extensions named simply “OCaml” or “OCaml and Reason IDE” are not the right ones. They are both old and no longer maintained by their developers.
简单命名为“OCaml”或“OCaml 和 Reason IDE”的扩展不是正确的扩展。它们都很旧并且不再由开发人员维护。

Warning 警告

Windows only: make sure you install the OCaml Platform extension using a button that says “Install in WSL: Ubuntu”, not with a button that says only “Install”. If you’ve already done the latter, that’s okay; but, you need to also install on WSL. If you can’t find a button that says “Install in WSL” then you probably do not have the green “WSL: Ubuntu” indicator at the bottom-left of your VS Code window, either. Follow the instructions above to open a WSL window. From there, try again to install the OCaml Platform extension using the “Install in WSL” button.
仅限 Windows:确保使用显示“Install in WSL: Ubuntu”的按钮安装 OCaml Platform 扩展,而不是使用仅显示“Install”的按钮。如果您已经完成了后者,那也没关系;但是,您还需要在 WSL 上安装。如果您找不到“在 WSL 中安装”按钮,那么您的 VS Code 窗口左下角可能也没有绿色的“WSL:Ubuntu”指示器。按照上述说明打开 WSL 窗口。从那里,再次尝试使用“Install in WSL”按钮安装 OCaml Platform 扩展。

Double Check VS Code
仔细检查 VS 代码 ¶

Let’s make sure VS Code’s OCaml support is working.
让我们确保 VS Code 的 OCaml 支持正常工作。

  • Reboot your computer again. (Yeah, that really shouldn’t be necessary. But it will detect so many potential mistakes now that it’s worth the effort.)
    再次重新启动计算机。 (是的,这确实没有必要。但它会检测到很多潜在的错误,所以值得付出努力。)

  • Open a fresh new Unix shell. Windows: remember that’s the Ubuntu, not PowerShell or Cmd. Mac: remember that you shouldn’t be manually switching to a different shell by typing zsh or bash.
    打开一个全新的 Unix shell。 Windows:记住那是 Ubuntu,而不是 PowerShell 或 Cmd。 Mac:请记住,您不应该通过键入 zshbash 手动切换到不同的 shell。

  • Navigate to a directory of your choice, preferably a subdirectory of your home directory. For example, you might create a directory for your 3110 work inside your home directory:
    导航到您选择的目录,最好是主目录的子目录。例如,您可以在主目录中为 3110 工作创建一个目录:

    mkdir ~/3110
    cd ~/3110
    

    In that directory open VS Code by running:
    在该目录中运行以下命令打开 VS Code:

    code .
    

    Go to File → New File. Save the file with the name test.ml. VS Code should give it an orange camel icon.
    转到文件 → 新建文件。使用名称 test.ml 保存文件。 VS Code 应该给它一个橙色的骆驼图标。

  • Type the following OCaml code then press Return/Enter:
    输入以下 OCaml 代码,然后按 Return/Enter:

    let x : int = 3110
    

    As you type, VS Code should colorize the syntax, suggest some completions, and add a little annotation above the line of code. Try changing the int you typed to string. A squiggle should appear under 3110. Hover over it to see the error message. Go to View → Problems to see it there, too. Add double quotes around the integer to make it a string, and the problem will go away.
    当您键入时,VS Code 应该对语法进行着色,建议一些补全,并在代码行上方添加一些注释。尝试将您输入的 int 更改为 string3110 下应出现一条曲线。将鼠标悬停在其上即可查看错误消息。也可以转到查看 → 问题来查看它。在整数周围添加双引号以使其成为字符串,问题就会消失。

If you don’t observe those behaviors, something is wrong with your install. Here’s how to proceed:
如果您没有观察到这些行为,则表明您的安装有问题。以下是如何进行:

  • Make sure that, from the same Unix prompt as which you launched VS Code, you can successfully complete the double-check instructions for your OPAM switch: can you run utop? is the right switch active? If not, that’s the problem you need to solve first. Then return to the VS Code issue. It might be fixed now.
    确保从启动 VS Code 的同一 Unix 提示符中,您可以成功完成 OPAM 开关的双重检查指令:您可以运行 utop 吗?右侧开关是否处于活动状态?如果没有,这就是您需要首先解决的问题。然后回到VS Code的问题。现在可能已经修复了。

  • If you’re on WSL and VS Code does add syntax highlighting but does not add squiggles as described above, and/or you get an error about “Sandbox initialization failed”, then double-check that you see a green “WSL” indicator in the bottom left of the VS Code window. If you do not, make sure you installed the “Remote - WSL” extension as described above, and that you are launching VS Code from Ubuntu rather than PowerShell or from the Windows GUI.
    如果您使用 WSL 并且 VS Code 确实添加了语法突出显示,但没有添加如上所述的波浪线,和/或您收到有关“沙箱初始化失败”的错误,请仔细检查您是否在VS Code 窗口的左下角。如果没有,请确保您安装了如上所述的“Remote - WSL”扩展,并且从 Ubuntu 而不是 PowerShell 或 Windows GUI 启动 VS Code。

If you’re still stuck with an issue, try uninstalling VS Code, rebooting, and re-doing all the install instructions above from scratch. Pay close attention to any warnings or errors.
如果您仍然遇到问题,请尝试卸载 VS Code、重新启动并从头开始重新执行上述所有安装说明。密切注意任何警告或错误。

Warning 警告

While troubleshooting any VS Code issues, do not hardcode any paths in the VS Code settings file, despite any advice you might find online. That is a band-aid, not a cure of whatever the underlying problem really is. More than likely, the real problem is an OCaml environment issue that you can investigate with the OCaml double-check instructions above.
在对任何 VS Code 问题进行故障排除时,不要对 VS Code 设置文件中的任何路径进行硬编码,尽管您可能会在网上找到任何建议。这只是一个创可贴,并不能解决根本问题。真正的问题很可能是 OCaml 环境问题,您可以使用上面的 OCaml 双重检查说明进行调查。

VS Code Settings VS 代码设置 ¶

We recommend tweaking a few editor settings. Open the user settings JSON file by (i) going to View → Command Palette, (ii) typing “user settings json”, and (iii) selecting Open User Settings (JSON). Copy and paste these settings into the window:
我们建议调整一些编辑器设置。通过以下方式打开用户设置 JSON 文件:(i) 转至查看 → 命令面板,(ii) 键入“用户设置 json”,以及 (iii) 选择打开用户设置 (JSON)。将这些设置复制并粘贴到窗口中:

{
    "editor.tabSize": 2,
    "editor.rulers": [ 80 ],
    "editor.formatOnSave": true
}

Save the file and close the tab.
保存文件并关闭选项卡。

Using VS Code Collaboratively
协作使用 VS Code ¶

VS Code’s Live Share extension makes it easy and fun to collaborate on code with other humans. You can edit code together like collaborating inside a Google Doc. It even supports a shared voice channel, so there’s no need to spin up a separate Zoom call. To install Live Share:
VS Code 的 Live Share 扩展让与其他人协作编写代码变得简单而有趣。您可以一起编辑代码,就像在 Google 文档中协作一样。它甚至支持共享语音通道,因此无需启动单独的 Zoom 通话。要安装 Live Share:

  • Open the Extensions page in VS Code. Search for “Live Share Extension Pack”. Install it.
    在 VS Code 中打开扩展页面。搜索“Live Share 扩展包”。安装它。

  • The first time you use Live Share, you will be prompted to login. If you are a Cornell student, choose to login with your Microsoft account, not GitHub. Enter your Cornell NetID email, e.g., your_netid@cornell.edu. That will take you to Cornell’s login site. Use the password associated with your NetID.
    第一次使用 Live Share 时,系统会提示您登录。如果您是康奈尔大学的学生,请选择使用您的 Microsoft 帐户登录,而不是 GitHub。输入您的 Cornell NetID 电子邮件,例如 your_netid@cornell.edu 。这将带您进入康奈尔大学的登录网站。使用与您的 NetID 关联的密码。

To collaborate with Live Share:
要与 Live Share 协作:

  • The host starts the Live Share session. That generates a URL. Send the URL to the guests however you like (DM, email, etc.).
    主持人启动 Live Share 会话。这会生成一个 URL。以您喜欢的方式(DM、电子邮件等)将 URL 发送给客人。

  • The guest puts that URL into a browser or directly into VS Code, and connects to the shared programming session.
    来宾将该 URL 放入浏览器或直接放入 VS Code 中,然后连接到共享编程会话。

1. Better Programming Through OCaml
1. 通过 OCaml 更好地编程 ¶

Do you already know how to program in a mainstream language like Python or Java? Good. This book is for you. It’s time to learn how to program better. It’s time to learn a functional language, OCaml.
您已经知道如何使用 Python 或 Java 等主流语言进行编程吗?好的。这本书适合你。是时候学习如何更好地编程了。是时候学习一门函数式语言 OCaml 了。

Functional programming provides a different perspective on programming than what you have experienced so far. Adapting to that perspective requires letting go of old ideas: assignment statements, loops, classes and objects, among others. That won’t be easy.
函数式编程提供了与您迄今为止所经历的不同的编程视角。适应这种观点需要放弃旧的想法:赋值语句、循环、类和对象等等。这并不容易。

Nan-in, a Japanese master during the Meiji era (1868-1912), received a university professor who came to inquire about Zen. Nan-in served tea. He poured his visitor’s cup full, and then kept on pouring. The professor watched the overflow until he no longer could restrain himself. “It is overfull. No more will go in!” “Like this cup,” Nan-in said, “you are full of your own opinions and speculations. How can I show you Zen unless you first empty your cup?”
明治时代(1868-1912)的日本大师南隐接待了一位前来询问禅宗的大学教授。南院奉茶。他把客人的杯子倒满,然后继续倒。教授看着溢出的东西,直到他再也无法克制自己。 “已经满了。以后再也不敢进去了!” “就像这个杯子一样,”南隐说,“你充满了自己的观点和猜测。除非你先倒空你的杯子,否则我如何向你展示禅宗呢?”

I believe that learning OCaml will make you a better programmer. Here’s why:
我相信学习 OCaml 会让你成为一名更好的程序员。原因如下:

  • You will experience the freedom of immutability, in which the values of so-called “variables” cannot change. Goodbye, debugging.
    您将体验到不变性的自由,其中所谓“变量”的值不能改变。再见,调试。

  • You will improve at abstraction, which is the practice of avoiding repetition by factoring out commonality. Goodbye, bloated code.
    您将提高抽象能力,这是通过排除共性来避免重复的做法。再见,臃肿的代码。

  • You will be exposed to a type system that you will at first hate because it rejects programs you think are correct. But you will come to love it, because you will humbly realize it was right and your programs were wrong. Goodbye, failing tests.
    您将接触到一个您一开始会讨厌的类型系统,因为它拒绝您认为正确的程序。但你会逐渐爱上它,因为你会谦虚地意识到它是对的,而你的程序是错的。再见,失败的测试。

  • You will be exposed to some of the theory and implementation of programming languages, helping you to understand the foundations of what you are saying to the computer when you write code. Goodbye, mysterious and magic incantations.
    您将接触到编程语言的一些理论和实现,帮助您理解编写代码时对计算机所说内容的基础。再见,神秘而神奇的咒语。

All of those ideas can be learned in other contexts and languages. But OCaml provides an incredible opportunity to bundle them all together. OCaml will change the way you think about programming.
所有这些想法都可以在其他环境和语言中学习。但 OCaml 提供了一个将它们捆绑在一起的绝佳机会。 OCaml 将改变您思考编程的方式。

“A language that doesn’t affect the way you think about programming is not worth knowing.”
“一门不会影响你思考编程方式的语言是不值得了解的。”

—Alan J. Perlis (1922-1990), first recipient of the Turing Award
—Alan J. Perlis(1922-1990),第一位图灵奖获得者

Moreover, OCaml is beautiful. OCaml is elegant, simple, and graceful. Aesthetics do matter. Code isn’t written just to be executed by machines. It’s also written to communicate to humans. Elegant code is easier to read and maintain. It isn’t necessarily easier to write.
而且,OCaml 很漂亮。 OCaml 优雅、简单、优雅。审美确实很重要。编写代码不仅仅是为了让机器执行。它也是为了与人类交流而编写的。优雅的代码更容易阅读和维护。写起来并不一定容易。

The OCaml code you write can be stylish and tasteful. At first, this might not be apparent. You are learning a new language after all—you wouldn’t expect to appreciate Sanskrit poetry on day 1 of Introductory Sanskrit. In fact, you’ll likely feel frustrated for awhile as you struggle to express yourself in a new language. So give it some time. After you’ve mastered OCaml, you might be surprised at how ugly those other languages you already know end up feeling when you return to them.
您编写的 OCaml 代码可以时尚且有品味。起初,这可能并不明显。毕竟,您正在学习一门新语言 - 您不会期望在梵语入门的第一天欣赏梵语诗歌。事实上,当您努力用新语言表达自己时,您可能会感到沮丧一段时间。所以给它一些时间。掌握 OCaml 后,您可能会惊讶地发现,当您再次使用已经了解的其他语言时,它们最终会感觉多么丑陋。

1.1. The Past of OCaml
1.1. OCaml 的过去 ¶

Genealogically, OCaml comes from the line of programming languages whose grandfather is Lisp and includes other modern languages such as Clojure, F#, Haskell, and Racket.
从谱系上来看,OCaml 源自 Lisp 的编程语言系列,还包括其他现代语言,例如 Clojure、F#、Haskell 和 Racket。

OCaml originates from work done by Robin Milner and others at the Edinburgh Laboratory for Computer Science in Scotland. They were working on theorem provers in the late 1970s and early 1980s. Traditionally, theorem provers were implemented in languages such as Lisp. Milner kept running into the problem that the theorem provers would sometimes put incorrect “proofs” (i.e., non-proofs) together and claim that they were valid. So he tried to develop a language that only allowed you to construct valid proofs. ML, which stands for “Meta Language”, was the result of that work. The type system of ML was carefully constructed so that you could only construct valid proofs in the language. A theorem prover was then written as a program that constructed a proof. Eventually, this “Classic ML” evolved into a full-fledged programming language.
OCaml 源自 Robin Milner 和其他人在苏格兰爱丁堡计算机科学实验室所做的工作。他们在 20 世纪 70 年代末和 80 年代初致力于定理证明。传统上,定理证明器是用 Lisp 等语言实现的。米尔纳不断遇到这样的问题:定理证明者有时会将不正确的“证明”(即非证明)放在一起并声称它们是有效的。所以他尝试开发一种只允许你构建有效证明的语言。 ML(“元语言”的缩写)就是这项工作的成果。 ML 的类型系统经过精心构建,因此您只能用该语言构建有效的证明。然后将定理证明器编写为构建证明的程序。最终,这种“经典机器学习”演变成一种成熟的编程语言。

In the early ’80s, there was a schism in the ML community with the French on one side and the British and US on another. The French went on to develop CAML and later Objective CAML (OCaml) while the Brits and Americans developed Standard ML. The two dialects are quite similar. Microsoft introduced its own variant of OCaml called F# in 2005.
20 世纪 80 年代初,机器学习社区出现了分裂,一方面是法国人,另一方面是英国和美国。法国人继续开发 CAML 和后来的 Objective CAML (OCaml),而英国人和美国人开发了标准 ML。这两种方言非常相似。 Microsoft 于 2005 年推出了自己的 OCaml 变体,称为 F#。

Milner received the Turing Award in 1991 in large part for his work on ML. The ACM website for his award includes this praise:
Milner 于 1991 年获得图灵奖,很大程度上是因为他在 ML 方面的工作。 ACM 网站对他的获奖做出了这样的赞扬:

ML was way ahead of its time. It is built on clean and well-articulated mathematical ideas, teased apart so that they can be studied independently and relatively easily remixed and reused. ML has influenced many practical languages, including Java, Scala, and Microsoft’s F#. Indeed, no serious language designer should ignore this example of good design.
机器学习远远领先于时代。它建立在清晰且清晰的数学思想之上,经过梳理,以便可以独立研究它们,并且相对容易地重新混合和重用。 ML 影响了许多实用语言,包括 Java、Scala 和 Microsoft 的 F#。事实上,任何严肃的语言设计者都不应该忽视这个优秀设计的例子。

1.2. The Present of OCaml
1.2. OCaml 的现在 ¶

OCaml is a functional programming language. The key linguistic abstraction of functional languages is the mathematical function. A function maps an input to an output; for the same input, it always produces the same output. That is, mathematical functions are
OCaml 是一种函数式编程语言。函数式语言的关键语言抽象是数学函数。函数将输入映射到输出;对于相同的输入,它总是产生相同的输出。也就是说,数学函数是
stateless 无国籍的: they do not maintain any extra information or
:他们不保留任何额外信息或
state 状态 that persists between usages of the function. Functions are
在函数的使用之间持续存在。功能有
first-class 头等舱: you can use them as input to other functions, and produce functions as output. Expressing everything in terms of functions enables a uniform and simple programming model that is easier to reason about than the procedures and methods found in other families of languages.
:您可以将它们用作其他函数的输入,并生成函数作为输出。用函数来表达一切可以实现统一且简单的编程模型,该模型比其他语言系列中的过程和方法更容易推理。

OCaml 是一种函数式编程语言。函数式语言的关键语言抽象是数学函数。函数将输入映射到输出;对于相同的输入,它总是产生相同的输出。也就是说,数学函数是无状态的:它们不维护在函数使用之间持续存在的任何额外信息或状态。函数是一流的:您可以将它们用作其他函数的输入,并生成函数作为输出。用函数来表达一切可以实现统一且简单的编程模型,该模型比其他语言系列中的过程和方法更容易推理。

Imperative programming languages such as C and Java involve mutable state that changes throughout execution. Commands specify how to compute by destructively changing that state. Procedures (or methods) can have side effects that update state in addition to producing a return value.
命令式编程语言(例如 C 和 Java)涉及在执行过程中发生变化的可变状态。命令指定如何通过破坏性地改变该状态来进行计算。过程(或方法)除了产生返回值之外还可能具有更新状态的副作用。

The fantasy of mutability is that it’s easy to reason about: the machine does this, then this, etc.
可变性的幻想在于它很容易推理:机器做这个,然后做这个,等等。

The reality of mutability is that whereas machines are good at complicated manipulation of state, humans are not good at understanding it. The essence of why that’s true is that mutability breaks referential transparency: the ability to replace an expression with its value without affecting the result of a computation. In math, if f(x)=y, then you can substitute y anywhere you see f(x). In imperative languages, you cannot: f might have side effects, so computing f(x) at time t might result in a different value than at time t.
可变性的现实是,虽然机器擅长复杂的状态操纵,但人类却不擅长理解它。其本质是可变性破坏了引用透明性:用表达式的值替换表达式而不影响计算结果的能力。在数学中,如果 f(x)=y ,那么您可以在看到 f(x) 的任何地方替换 y 。在命令式语言中,您不能: f 可能会产生副作用,因此在时间 t 计算 f(x) 可能会产生与时间 t

It’s tempting to believe that there’s a single state that the machine manipulates, and that the machine does one thing at a time. Computer systems go to great lengths in attempting to provide that illusion. But it’s just that: an illusion. In reality, there are many states, spread across threads, cores, processors, and networked computers. And the machine does many things concurrently. Mutability makes reasoning about distributed state and concurrent execution immensely difficult.
人们很容易相信机器操纵的是单一状态,并且机器一次只做一件事。计算机系统竭尽全力试图提供这种幻觉。但这只是:一种幻觉。实际上,存在许多状态,分布在线程、内核、处理器和联网计算机上。机器可以同时做很多事情。可变性使得分布式状态和并发执行的推理变得非常困难。

Immutability, however, frees the programmer from these concerns. It provides powerful ways to build correct and concurrent programs. OCaml is primarily an immutable language, like most functional languages. It does support imperative programming with mutable state, but we won’t use those features until many chapters into the book—in part because we simply won’t need them, and in part to get you to quit “cold turkey” from a dependence you might not have known that you had. This freedom from mutability is one of the biggest changes in perspective that OCaml can give you.
然而,不变性使程序员摆脱了这些担忧。它提供了构建正确的并发程序的强大方法。与大多数函数式语言一样,OCaml 主要是一种不可变语言。它确实支持具有可变状态的命令式编程,但是直到本书的许多章节中我们才会使用这些功能,部分原因是我们根本不需要它们,部分原因是为了让您从依赖中“突然戒掉”你可能不知道你有。这种免于可变性的自由是 OCaml 可以为您带来的最大的视角变化之一。

1.2.1. The Features of OCaml
1.2.1. OCaml 的特点 ¶

OCaml is a  OCaml 是一个statically-typed 静态类型 and type-safe 类型安全 programming language. A statically-typed language detects type errors at compile time; if a type error is detected, the language won’t allow execution of the program. A type-safe language limits which kinds of operations can be performed on which kinds of data. In practice, this prevents a lot of silly errors (e.g., treating an integer as a function) and also prevents a lot of security problems: over half of the reported break-ins at the Computer Emergency Response Team (CERT, a US government agency tasked with cybersecurity) were due to buffer overflows, something that’s impossible in a type-safe language.
编程语言。静态类型语言在编译时检测类型错误;如果检测到类型错误,该语言将不允许执行该程序。类型安全语言限制可以对哪种数据执行哪种操作。实际上,这可以防止许多愚蠢的错误(例如,将整数视为函数),并且还可以防止许多安全问题:计算机紧急响应小组(CERT,美国政府机构)报告的入侵事件中有一半以上负责网络安全的任务)是由于缓冲区溢出造成的,这在类型安全语言中是不可能的。

OCaml 是一种静态类型和类型安全的编程语言。静态类型语言在编译时检测类型错误;如果检测到类型错误,该语言将不允许执行该程序。类型安全语言限制可以对哪种数据执行哪种操作。实际上,这可以防止许多愚蠢的错误(例如,将整数视为函数),并且还可以防止许多安全问题:计算机紧急响应小组(CERT,美国政府机构)报告的入侵事件中有一半以上负责网络安全的任务)是由于缓冲区溢出造成的,这在类型安全语言中是不可能的。

Some languages, like Python and Racket, are type-safe but dynamically typed. That is, type errors are caught only at run time. Other languages, like C and C++, are statically typed but not type safe: they check for some type errors, but don’t guarantee the absence of all
C 和 C++ 是静态类型的,但不是类型安全的:它们检查某些类型错误,但不保证不存在所有类型错误
type errors. That is, there’s no guarantee that a type error won’t occur at run time. And still other languages, like Java, use a combination of static and dynamic typing to achieve type safety.
有些语言(例如 Python 和 Racket)是类型安全的,但是是动态类型的。也就是说,类型错误仅在运行时才会被捕获。其他语言(例如 C 和 C++)是静态类型的,但不是类型安全的:它们检查某些类型错误,但不保证不存在所有类型错误。也就是说,不能保证在运行时不会发生类型错误。还有其他语言,如 Java,使用静态和动态类型的组合来实现类型安全。

OCaml supports a number of advanced features, some of which you will have encountered before, and some of which are likely to be new:
OCaml 支持许多高级功能,其中一些您以前会遇到过,而另一些可能是新的:

  • Algebraic datatypes: You can build sophisticated data structures in OCaml easily, without fussing with pointers and memory management. Pattern matching 模式匹配—a feature we’ll soon learn about that enables examining the shape of a data structure—makes them even more convenient.
    代数数据类型:您可以在 OCaml 中轻松构建复杂的数据结构,而无需担心指针和内存管理。模式匹配——我们很快就会了解的一个功能,可以检查数据结构的形状——使它们变得更加方便。

  • Type inference 类型推断: You do not have to write type information down everywhere. The compiler automatically figures out most types. This can make the code easier to read and maintain.
    类型推断:您不必在任何地方都写下类型信息。编译器会自动找出大多数类型。这可以使代码更易于阅读和维护。

  • Parametric polymorphism: 参数多态性: Functions and data structures can be parameterized over types. This is crucial for being able to re-use code.
    函数和数据结构可以通过类型进行参数化。这对于能够重用代码至关重要。

    参数多态性:函数和数据结构可以通过类型进行参数化。这对于能够重用代码至关重要。

  • Garbage collection: Automatic memory management relieves you from the burden of memory allocation and deallocation, a common source of bugs in languages such as C.
    垃圾收集:自动内存管理使您摆脱内存分配和释放的负担,这是 C 等语言中错误的常见来源。

  • Modules: OCaml makes it easy to structure large systems through the use of modules. Modules are used to encapsulate implementations behind interfaces. OCaml goes well beyond the functionality of most languages with modules by providing functions (called functors) that manipulate modules.
    模块:OCaml 通过使用模块可以轻松构建大型系统。模块用于封装接口后面的实现。 OCaml 通过提供操作模块的函数(称为函子),远远超出了大多数带有模块的语言的功能。

1.2.2. OCaml in Industry
1.2.2. 工业中的 OCaml ¶

OCaml and other functional languages are nowhere near as popular as Python, C, or Java. OCaml’s real strength lies in language manipulation (i.e., compilers, analyzers, verifiers, provers, etc.). This is not surprising, because OCaml evolved from the domain of theorem proving.
OCaml 和其他函数式语言远不如 Python、C 或 Java 流行。 OCaml 的真正优势在于语言操作(即编译器、分析器、验证器、证明器等)。这并不奇怪,因为 OCaml 是从定理证明领域发展而来的。

That’s not to say that functional languages aren’t used in industry. There are many industry projects using OCaml and Haskell, among other languages. Yaron Minsky (Cornell PhD ‘02) even wrote a paper about using OCaml in the financial industry. It explains how the features of OCaml make it a good choice for quickly building complex software that works.
这并不是说函数式语言没有在工业中使用。有许多行业项目使用 OCaml 和 Haskell 等语言。 Yaron Minsky(康奈尔大学博士 02)甚至写了一篇关于在金融行业使用 OCaml 的论文。它解释了 OCaml 的功能如何使其成为快速构建可用的复杂软件的良好选择。

1.3. Look to Your Future
1.3. 展望你的未来 ¶

General-purpose languages come and go. In your life you’ll likely learn a handful. Today, it’s Python and Java. Yesterday, it was Pascal and Cobol. Before that, it was Fortran and Lisp. Who knows what it will be tomorrow? In this fast-changing field you need to be able to rapidly adapt. A good programmer has to learn the principles behind programming that transcend the specifics of any specific language. There’s no better way to get at these principles than to approach programming from a functional perspective. Learning a new language from scratch affords the opportunity to reflect along the way about the difference between programming and programming in a language.
通用语言来来去去。在你的生活中,你可能会学到一些东西。今天,是 Python 和 Java。昨天,是帕斯卡和科博尔。在此之前,是 Fortran 和 Lisp。谁知道明天会怎样?在这个快速变化的领域,您需要能够快速适应。优秀的程序员必须学习超越任何特定语言细节的编程背后的原理。要了解这些原则,没有比从功能角度进行编程更好的方法了。从头开始学习一门新语言提供了一个机会来反思编程和用某种语言编程之间的差异。

If after OCaml you want to learn more about functional programming, you’ll be well prepared. OCaml does a great job of clarifying and simplifying the essence of functional programming in a way that other languages that blend functional and imperative programming (like Scala) or take functional programming to the extreme (like Haskell) do not.
如果在 OCaml 之后您想了解更多有关函数式编程的知识,那么您已经做好了充分的准备。 OCaml 在阐明和简化函数式编程的本质方面做得非常出色,这是其他混合函数式和命令式编程(如 Scala)或将函数式编程发挥到极致(如 Haskell)的语言所没有的。

And even if you never code in OCaml again after learning it, you’ll still be better prepared for the future. Advanced features of functional languages have a surprising tendency to predict new features of more mainstream languages. Java brought garbage collection into the mainstream in 1995; Lisp had it in 1958. Java didn’t have generics until version 5 in 2004; the ML family had it in 1990. First-class functions and type inference have been incorporated into mainstream languages like Java, C#, and C++ over the last 10 years, long after functional languages introduced them.
即使您在学习 OCaml 后不再使用 OCaml 进行编码,您仍然可以为未来做好更好的准备。函数式语言的高级特性具有预测更主流语言的新特性的惊人趋势。 Java 于 1995 年将垃圾收集带入主流; Lisp 在 1958 年就拥有了泛型。Java 直到 2004 年的第 5 版才拥有泛型。 ML 家族在 1990 年就拥有了它。在过去 10 年里,一流的函数和类型推断已经被纳入 Java、C# 和 C++ 等主流语言中,而在函数式语言引入它们很久之后。

News Flash! 新闻快讯!

Python just announced plans to support pattern matching in February 2021.
Python 刚刚宣布计划于 2021 年 2 月支持模式匹配。

1.4. A Brief History of CS 3110
1.4. CS 3110 简史 ¶

This book is the primary textbook for CS 3110 at Cornell University. The course has existed for over two decades and has always taught functional programming, but it has not always used OCaml.
本书是康奈尔大学CS 3110的主要教材。该课程已经存在了二十多年,一直教授函数式编程,但并不总是使用 OCaml。

Once upon a time, there was a course at MIT known as 6.001 Structure and Interpretation of Computer Programs (SICP). It had a textbook by the same name, and it used Scheme, a functional programming language. Tim Teitelbaum taught a version of the course at Cornell in Fall 1988, following the book rather closely and using Scheme.
曾几何时,麻省理工学院有一门名为 6.001 计算机程序结构和解释 (SICP) 的课程。它有一本同名的教科书,并且使用了Scheme,一种函数式编程语言。 1988 年秋季,蒂姆·泰特尔鲍姆 (Tim Teitelbaum) 在康奈尔大学教授了该课程的一个版本,他相当严格地遵循这本书并使用了方案。

CS 212. Dan Huttenlocher had been a TA for 6.001 at MIT; he later became faculty at Cornell. In Fall 1989, he inaugurated CS 212 Modes of Algorithm Expression. Basing the course on SICP, he infused a more rigorous approach to the material. Huttenlocher continued to develop CS 212 through the mid 1990s, using various homegrown dialects of Scheme.
CS 212. Dan Huttenlocher 曾在麻省理工学院担任助教 6.001;他后来成为康奈尔大学的教员。 1989 年秋季,他开创了 CS 212 算法表达模式。他以 SICP 为基础,为课程注入了更严格的方法。 Huttenlocher 在 20 世纪 90 年代中期继续开发 CS 212,使用了各种本土的 Scheme 方言。

Other faculty began teaching the course regularly. Ramin Zabih had taken 6.001 as a first-year student at MIT. In Spring 1994, having become faculty at Cornell, he taught CS 212. Dexter Kozen (Cornell PhD 1977) first taught the course in Spring 1996. The earliest surviving online record of the course seems to be Spring 1998, which was taught by Greg Morrisett in Dylan; the name of the course had become Structure and Interpretation of Computer Programs.
其他教师开始定期教授该课程。 Ramin Zabih 作为麻省理工学院一年级学生取得了 6.001 分。 1994 年春,成为康奈尔大学教员后,他教授 CS 212。Dexter Kozen(康奈尔大学博士 1977 年)于 1996 年春首次教授该课程。该课程最早的在线记录似乎是 1998 年春季,由 Greg Morrisett 教授在迪伦;该课程的名称已成为“计算机程序的结构和解释”。

By Fall 1999, CS 212 had its own lecture notes. As CS 3110 still does, that instance of CS 212 covered functional programming, the substitution and environment models, some data structures and algorithms, and programming language implementation.
到 1999 年秋季,CS 212 有了自己的讲义。与 CS 3110 一样,CS 212 的实例涵盖了函数式编程、替代和环境模型、一些数据结构和算法以及编程语言实现。

CS 312. At that time, the CS curriculum had two introductory programming courses, CS 211 Computers and Programming, and CS 212. Students took one or the other, similar to how students today take either CS 2110 or CS 2112. Then they took CS 410 Data Structures. The earliest surviving online record of CS 410 seems to be from Spring 1998. It covered many data structures and algorithms not covered by CS 212, including balanced trees and graphs, and it used Java as the programming language.
CS 312。当时,CS 课程有两门入门编程课程,CS 211 计算机与编程和 CS 212。学生选修其中一门,类似于今天学生选修 CS 2110 或 CS 2112。然后他们选修 CS 410 数据结构。 CS 410 最早的在线记录似乎来自 1998 年春季。它涵盖了 CS 212 未涵盖的许多数据结构和算法,包括平衡树和图,并且使用 Java 作为编程语言。

Depending on which course they took, CS 211 or 212, students were entering upper-level courses with different skill sets. After extensive discussions, the faculty chose to make CS 211 required, to rename CS 212 into CS 312 Data Structures and Functional Programming, and to make CS 211 a prerequisite for CS 312. At the same time, CS 410 was eliminated from the curriculum and its contents parceled out to CS 312 and CS 482 Introduction to Analysis of Algorithms. Dexter Kozen taught the final offering of CS 410 in Fall 1999.
根据他们选修的课程(CS 211 或 212),学生将进入具有不同技能组合的高级课程。经过广泛讨论,教师们决定将 CS 211 作为必修课,将 CS 212 更名为 CS 312 数据结构和函数式编程,并将 CS 211 作为 CS 312 的先决条件。同时,CS 410 从课程中删除,其内容分为 CS 312 和 CS 482 算法分析简介。 Dexter Kozen 于 1999 年秋季教授了 CS 410 的最后课程。

Greg Morrisett inaugurated the new CS 312 in Spring 2001. He switched from Scheme to Standard ML. Kozen first taught it in Fall 2001, and Andrew Myers in Fall 2002. Myers began to incorporate material on modular programming from another MIT textbook, Program Development in Java: Abstraction, Specification, and Object-Oriented Design by Barbara Liskov and John Guttag. Huttenlocher first taught the course in Spring 2006.
Greg Morrisett 于 2001 年春季推出了新的 CS 312。他从方案转向了标准 ML。 Kozen 于 2001 年秋季首次教授该课程,Andrew Myers 则于 2002 年秋季首次教授该课程。Myers 开始结合来自另一本 MIT 教科书《Java 程序开发:抽象、规范和面向对象设计》(Barbara Liskov 和 John Guttag)中的模块化编程材料。 Huttenlocher 于 2006 年春季首次教授该课程。

CS 3110. In Fall 2008 two big changes came: the language switched to OCaml, and the university switched to four-digit course numbers. CS 312 became CS 3110. Myers, Huttenlocher, Kozen, and Zabih first taught the revised course in Fall 2008, Spring 2009, Fall 2009, and Fall 2010, respectively. Nate Foster first taught the course in Spring 2012; and Bob Constable and Michael George co-taught for the first time in Fall 2013.
CS 3110。2008 年秋季发生了两项重大变化:语言切换为 OCaml,大学切换为四位数课程编号。 CS 312 成为 CS 3110。Myers、Huttenlocher、Kozen 和 Zabih 分别于 2008 年秋季、2009 年春季、2009 年秋季和 2010 年秋季首次教授修订后的课程。 Nate Foster 于 2012 年春季首次教授该课程;鲍勃·康斯特勃尔 (Bob Constable) 和迈克尔·乔治 (Michael George) 于 2013 年秋季首次共同授课。

Michael Clarkson (Cornell PhD 2010) first taught the course in Fall 2014, after having first TA’d the course as a PhD student back in Spring 2008. He began to revise the presentation of the OCaml programming material to incorporate ideas by Dan Grossman (Cornell PhD 2003) about a principled approach to learning a programming language by decomposing it into syntax, dynamic, and static semantics. Grossman uses that approach in CSE 341 Programming Languages at the University of Washington and in his popular Programming Languages MOOC.
Michael Clarkson(康奈尔大学 2010 年博士)在 2008 年春季作为博士生首次担任该课程的助教后,于 2014 年秋季首次教授该课程。他开始修改 OCaml 编程材料的演示文稿,以纳入 Dan Grossman 的想法(康奈尔大学博士 2003)介绍了一种通过将编程语言分解为语法、动态和静态语义来学习编程语言的原则性方法。格罗斯曼在华盛顿大学的 CSE 341 编程语言和他流行的编程语言 MOOC 中使用了这种方法。

In Fall 2018 the compilation of this textbook began. It synthesizes the work of over two decades of functional programming instruction at Cornell. In the words of the Cornell Evening Song,
2018年秋季,该教材的编写工作开始。它综合了康奈尔大学二十多年来函数式编程教学的成果。用康奈尔晚歌的话来说,

‘Tis an echo from the walls
这是来自墙壁的回声

Of our own, our fair Cornell.
我们自己的,我们美丽的康奈尔大学。

1.5. Summary 1.5. 小结 ¶

This book is about becoming a better programmer. Studying functional programming will help with that. The biggest obstacle in our way is the frustration of speaking a new language, particularly letting go of mutable state. But the benefits will be great: a discovery that programming transcends programming in any particular language or family of languages, an exposure to advanced language features, and an appreciation of beauty.
这本书是关于成为一名更好的程序员的。学习函数式编程将对此有所帮助。我们路上最大的障碍是说一门新语言的挫败感,特别是放弃可变状态。但好处将是巨大的:发现编程超越任何特定语言或语言族的编程,接触高级​​语言功能以及对美的欣赏。

1.5.1. Terms and Concepts
1.5.1. 术语和概念 ¶

  • dynamic typing 动态类型

  • first-class functions 一流的功能

  • functional programming languages
    函数式编程语言

  • immutability 不变性

  • Lisp 口齿不清

  • ML

  • OCaml 奥卡米尔

  • referential transparency 参考透明度

  • side effects 副作用 副作用

  • state 状态

  • static typing 静态类型

  • type safety 类型安全

1.5.2. Further Reading 1.5.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapters 1 and 2, a freely available textbook that is recommended for this course
    Objective Caml 简介,第 1 章和第 2 章,本课程推荐的免费教科书

  • OCaml from the Very Beginning, chapter 1, a textbook that is very gentle and recommended for this course. The PDF and HTML formats of the book are free of charge.
    OCaml 从头开始​​,第 1 章,一本非常温和的教科书,推荐用于本课程。本书的 PDF 和 HTML 格式都是免费的。

  • A guided tour [of OCaml]: chapter 1 of Real World OCaml, a book written by some Cornellians that some students might enjoy reading
    [OCaml] 导览:《真实世界 OCaml》的第一章,这是一本由一些康奈尔人写的书,一些学生可能会喜欢阅读

  • The history of Standard ML: though it focuses on the SML variant of the ML language, it’s relevant to OCaml
    标准 ML 的历史:虽然它重点关注 ML 语言的 SML 变体,但它与 OCaml 相关

  • The value of values: a lecture by the designer of Clojure (a modern dialect of Lisp) on how the time of imperative programming has passed
    价值观的价值:Clojure(Lisp 的现代方言)设计者关于命令式编程时代如何过去的讲座

  • Teach yourself programming in 10 years: an essay by a Director of Research at Google that puts the time required to become an educated programmer into perspective
    10 年内自学编程:谷歌研究总监的一篇文章,阐述了成为受过教育的程序员所需的时间

2. The Basics of OCaml
2. OCaml 基础知识 ¶

This chapter will cover some of the basic features of OCaml. But before we dive in to learning OCaml, let’s first talk about a bigger idea: learning languages in general.
本章将介绍 OCaml 的一些基本功能。但在我们深入学习 OCaml 之前,让我们首先讨论一个更大的想法:学习一般语言。

One of the secondary goals of this course is not just for you to learn a new programming language, but to improve your skills at learning how to learn new languages.
本课程的次要目标之一不仅仅是让您学习一门新的编程语言,而是提高您学习如何学习新语言的技能。

There are five essential components to learning a language: syntax, semantics, idioms, libraries, and tools.
学习语言有五个基本组成部分:语法、语义、习语、库和工具。

Syntax. By syntax, we mean the rules that define what constitutes a textually well-formed program in the language, including the keywords, restrictions on whitespace and formatting, punctuation, operators, etc. One of the more annoying aspects of learning a new language can be that the syntax feels odd compared to languages you already know. But the more languages you learn, the more you’ll become used to accepting the syntax of the language for what it is, rather than wishing it were different. (If you want to see some languages with really unusual syntax, take a look at APL, which needs its own extended keyboard, and Whitespace, in which programs consist entirely of spaces, tabs, and newlines.) You need to understand syntax just to be able to speak to the computer at all.
句法。通过语法,我们指的是定义该语言中文本格式良好的程序的规则,包括关键字、对空格和格式的限制、标点符号、运算符等。学习一门新语言的一个更烦人的方面可能是与您已经了解的语言相比,其语法感觉很奇怪。但是,你学习的语言越多,你就越会习惯于接受该语言的语法,而不是希望它有所不同。 (如果您想了解一些具有非常不寻常语法的语言,请查看 APL,它需要自己的扩展键盘,以及 Whitespace,其中程序完全由空格、制表符和换行符组成。)您需要了解语法只是为了完全能够与计算机对话。

Semantics. By semantics, we mean the rules that define the behavior of programs. In other words, semantics is about the meaning of a program—what computation a particular piece of syntax represents. Note that although “semantics” is plural in form, we use it as singular. That’s similar to “mathematics” or “physics”.
语义学。通过语义,我们指的是定义程序行为的规则。换句话说,语义是关于程序的含义——特定语法片段代表什么计算。请注意,虽然“语义”在形式上是复数,但我们将其用作单数。这类似于“数学”或“物理”。

There are two pieces to semantics, the dynamic semantics of a language and the static semantics of a language. The dynamic semantics define the run-time behavior of a program as it is executed or evaluated. The static semantics define the compile-time checking that is done to ensure that a program is legal, beyond any syntactic requirements. The most important kind of static semantics is probably type checking: the rules that define whether a program is well typed or not. Learning the semantics of a new language is usually the real challenge, even though the syntax might be the first hurdle you have to overcome. You need to understand semantics to say what you mean to the computer, and you need to say what you mean so that your program performs the right computation.
语义有两部分:语言的动态语义和语言的静态语义。动态语义定义了程序在执行或求值时的运行时行为。静态语义定义了编译时检查,以确保程序合法,超出任何语法要求。最重要的静态语义可能是类型检查:定义程序是否类型良好的规则。学习新语言的语义通常是真正的挑战,尽管语法可能是您必须克服的第一个障碍。您需要理解语义才能说出您对计算机的意思,并且您需要说出您的意思以便您的程序执行正确的计算。

Idioms. By idioms, we mean the common approaches to using language features to express computations. Given that you might express one computation in many ways inside a language, which one do you choose? Some will be more natural than others. Programmers who are fluent in the language will prefer certain modes of expression over others. We could think of this in terms of using the dominant paradigms in the language effectively, whether they are imperative, functional, object oriented, etc. You need to understand idioms to say what you mean not just to the computer, but to other programmers. When you write code idiomatically, other programmers will understand your code better.
习语。通过习语,我们指的是使用语言特征来表达计算的常见方法。鉴于您可能会在一种语言中以多种方式表达一种计算,您会选择哪一种?有些会比其他的更自然。精通该语言的程序员会比其他表达方式更喜欢某些表达方式。我们可以从有效使用语言中的主导范式的角度来考虑这一点,无论它们是命令式的、函数式的、面向对象的等等。您需要理解惯用法,以便不仅对计算机而且对其他程序员表达您的意思。当您以惯用的方式编写代码时,其他程序员会更好地理解您的代码。

Libraries. Libraries are bundles of code that have already been written for you and can make you a more productive programmer, since you won’t have to write the code yourself. (It’s been said that laziness is a virtue for a programmer.) Part of learning a new language is discovering what libraries are available and how to make use of them. A language usually provides a standard library that gives you access to a core set of functionality, much of which you would be unable to code up in the language yourself, such as file I/O.
图书馆。库是已经为您编写的代码包,可以使您成为更有生产力的程序员,因为您不必自己编写代码。 (据说懒惰是程序员的一种美德。)学习一门新语言的一部分是发现哪些库可用以及如何使用它们。一种语言通常提供一个标准库,使您可以访问一组核心功能,其中大部分功能您无法自己用该语言进行编码,例如文件 I/O。

Tools. At the very least any language implementation provides either a compiler or interpreter as a tool for interacting with the computer using the language. But there are other kinds of tools: debuggers; integrated development environments (IDE); and analysis tools for things like performance, memory usage, and correctness. Learning to use tools that are associated with a language can also make you a more productive programmer. Sometimes it’s easy to confuse the tool itself for the language; if you’ve only ever used Eclipse and Java together for example, it might not be apparent that Eclipse is an IDE that works with many languages, and that Java can be used without Eclipse.
工具。至少任何语言实现都提供编译器或解释器作为使用该语言与计算机交互的工具。但还有其他类型的工具:调试器;集成开发环境(IDE);以及性能、内存使用和正确性等分析工具。学习使用与语言相关的工具也可以使您成为更有生产力的程序员。有时很容易将工具本身与语言混淆;例如,如果您只一起使用过 Eclipse 和 Java,那么您可能不会明显看出 Eclipse 是一个可以与多种语言一起使用的 IDE,并且可以在没有 Eclipse 的情况下使用 Java。

When it comes to learning OCaml in this book, our focus is primarily on semantics and idioms. We’ll have to learn syntax along the way, of course, but it’s not the interesting part of our studies. We’ll get some exposure to the OCaml standard library and a couple other libraries, notably OUnit (a unit testing framework similar to JUnit, HUnit, etc.). Besides the OCaml compiler and build system, the main tool we’ll use is the toplevel, which provides the ability to interactively experiment with code.
在本书中学习 OCaml 时,我们的重点主要是语义和习惯用法。当然,我们必须一路学习语法,但这不是我们研究的有趣部分。我们将接触一些 OCaml 标准库和其他几个库,特别是 OUnit(类似于 JUnit、HUnit 等的单元测试框架)。除了 OCaml 编译器和构建系统之外,我们将使用的主要工具是顶层工具,它提供了交互式代码实验的能力。

2.1. The OCaml Toplevel
2.1. OCaml 顶层 ¶

The toplevel is like a calculator or command-line interface to OCaml. It’s similar to JShell for Java, or the interactive Python interpreter. The toplevel is handy for trying out small pieces of code without going to the trouble of launching the OCaml compiler. But don’t get too reliant on it, because creating, compiling, and testing large programs will require more powerful tools. Some other languages would call the toplevel a REPL, which stands for read-eval-print-loop: it reads programmer input, evaluates it, prints the result, and then repeats.
顶层就像 OCaml 的计算器或命令行界面。它类似于 Java 的 JShell 或交互式 Python 解释器。顶层可以方便地尝试小段代码,而无需启动 OCaml 编译器。但不要过于依赖它,因为创建、编译和测试大型程序将需要更强大的工具。其他一些语言将顶层称为 REPL,它代表 read-eval-print-loop:它读取程序员的输入,对其进行评估,打印结果,然后重复。

In a terminal window, type utop to start the toplevel. Press Control-D to exit the toplevel. You can also enter #quit;; and press return. Note that you must type the # there: it is in addition to the # prompt you already see.
在终端窗口中,输入 utop 以启动顶层。按 Control-D 退出顶层。您还可以输入 #quit;; 并按回车键。请注意,您必须在此处键入 # :它是您已经看到的 # 提示的补充。

2.1.1. Types and values
2.1.1. 类型和值 ¶

You can enter expressions into the OCaml toplevel. End an expression with a double semi-colon ;; and press the return key. OCaml will then evaluate the expression, tell you the resulting value, and the value’s type. For example:
您可以在 OCaml 顶层输入表达式。以双分号 ;; 结束表达式,然后按回车键。然后 OCaml 将计算表达式,告诉您结果值以及值的类型。例如:

# 42;;
- : int = 42

Let’s dissect that response from utop, reading right to left:
让我们从右向左仔细阅读 utop 的响应:

  • 42 is the value.
    42 是值。

  • int is the type of the value.
    int 是值的类型。

  • The value was not given a name, hence the symbol -.
    该值未指定名称,因此使用符号 -

That utop interaction was “hardcoded” as part of this book. We had to type in all the characters: the #, the -, etc. But the infrastructure used to write this book actually enables us to write code that is evaluated by OCaml at the time the book is translated into HTML or PDF. From now on, that’s usually what we will do. It looks like this:
utop 交互被“硬编码”为本书的一部分。我们必须输入所有字符: #- 等。但是用于编写本书的基础设施实际上使我们能够编写由 OCaml 在书被翻译成 HTML 或 PDF 的时间。从现在开始,这就是我们通常会做的事情。它看起来像这样:

42
- : int = 42

The first code block with the 42 in it is the code we asked OCaml to run. If you want to enter that into utop, you can copy and paste it. There’s an icon in the top right of the block to do that easily. Just remember to add the double semicolon at the end. The second code block, which is indented a little, is the output from OCaml as the book was being translated.
第一个带有 42 的代码块是我们要求 OCaml 运行的代码。如果您想将其输入到 utop 中,可以复制并粘贴它。该块的右上角有一个图标可以轻松完成此操作。只需记住在末尾添加双分号即可。第二个代码块稍微缩进了一点,是翻译本书时 OCaml 的输出。

Tip

If you’re viewing this in a web browser, look to the top right for a download icon. Choose the .md option, and you’ll see the original MyST Markdown source code for this page of the book. You’ll see that the output from the second example above is not actually present in the source code. That’s good! It means that the output stays consistent with whatever current version of the OCaml compiler we use to build the book. It also means that any compilation errors can be detected as part of building the book, instead of lurking for you, dear reader, to find them.
如果您在网络浏览器中查看此内容,请查看右上角的下载图标。选择 .md 选项,您将看到本书本页的原始 MyST Markdown 源代码。您将看到上面第二个示例的输出实际上并不存在于源代码中。那挺好的!这意味着输出与我们用来构建本书的 OCaml 编译器的当前版本保持一致。这也意味着任何编译错误都可以在编写本书的过程中被检测到,而不是潜伏着让你,亲爱的读者去发现它们。

You can bind values to names with a let definition, as follows:
您可以使用 let 定义将值绑定到名称,如下所示:

let x = 42
val x : int = 42

Again, let’s dissect that response, this time reading left to right:
再次,让我们剖析该响应,这次从左到右阅读:

  • A value was bound to a name, hence the val keyword.
    值绑定到名称,因此有 val 关键字。

  • x is the name to which the value was bound.
    x 是值绑定到的名称。

  • int is the type of the value.
    int 是值的类型。

  • 42 is the value.
    42 是值。

You can pronounce the entire output as “x has type int and equals 42.”
您可以将整个输出发音为“ x 具有类型 int 且等于 42 ”。

2.1.2. Functions 2.1.2. 函数 ¶

A function can be defined at the toplevel using syntax like this:
可以使用如下语法在顶层定义函数:

let increment x = x + 1
val increment : int -> int = <fun>

Let’s dissect that response:
让我们剖析一下这个回应:

  • increment is the identifier to which the value was bound.
    increment 是值绑定到的标识符。

  • int -> int is the type of the value. This is the type of functions that take an int as input and produce an int as output. Think of the arrow -> as a kind of visual metaphor for the transformation of one value into another value—which is what functions do.
    int -> int 是值的类型。这是一种将 int 作为输入并生成 int 作为输出的函数。将箭头 -> 视为一种将一个值转换为另一个值的视觉隐喻——这就是函数的作用。

  • The value is a function, which the toplevel chooses not to print (because it has now been compiled and has a representation in memory that isn’t easily amenable to pretty printing). Instead, the toplevel prints <fun>, which is just a placeholder.
    该值是一个函数,顶层选择不打印它(因为它现在已经被编译并且在内存中具有不容易进行漂亮打印的表示)。相反,顶层打印 <fun> ,这只是一个占位符。

Note 笔记

<fun> itself is not a value. It just indicates an unprintable function value.
<fun> 本身不是一个值。它只是表示一个不可打印的函数值。

You can “call” functions with syntax like this:
您可以使用如下语法“调用”函数:

increment 0
- : int = 1
increment(21)
- : int = 22
increment (increment 5)
- : int = 7

But in OCaml the usual vocabulary is that we “apply” the function rather than “call” it.
但在 OCaml 中,通常的词汇是我们“应用”函数而不是“调用”它。

Note how OCaml is flexible about whether you write the parentheses or not, and whether you write whitespace or not. One of the challenges of first learning OCaml can be figuring out when parentheses are actually required. So if you find yourself having problems with syntax errors, one strategy is to try adding some parentheses. The preferred style, though, is usually to omit parentheses when they are not needed. So, increment 21 is better than increment(21)
请注意 OCaml 在是否写括号以及是否写空格方面是如何灵活的。首次学习 OCaml 的挑战之一可能是弄清楚何时实际需要括号。因此,如果您发现自己遇到语法错误问题,一种策略是尝试添加一些括号。不过,首选的样式通常是在不需要时省略括号。因此, increment 21 优于 increment(21)
.

2.1.3. Loading code in the toplevel
2.1.3. 在顶层加载代码 ¶

In addition to allowing you to define functions, the toplevel will also accept directives that are not OCaml code but rather tell the toplevel itself to do something. All directives begin with the # character. Perhaps the most common directive is #use, which loads all the code from a file into the toplevel, just as if you had typed the code from that file into the toplevel.
除了允许您定义函数之外,顶层还将接受不是 OCaml 代码的指令,而是告诉顶层本身执行某些操作。所有指令均以 # 字符开头。也许最常见的指令是 #use ,它将文件中的所有代码加载到顶层,就像您将该文件中的代码键入到顶层一样。

For example, suppose you create a file named mycode.ml. In that file put the following code:
例如,假设您创建一个名为 mycode.ml 的文件。在该文件中放入以下代码:

let inc x = x + 1

Start the toplevel. Try entering the following expression, and observe the error:
开始顶层。尝试输入以下表达式,并观察错误:

inc 3
File "[7]", line 1, characters 0-3:
1 | inc 3
    ^^^
Error: Unbound value inc
Hint: Did you mean incr?

The error occurs because the toplevel does not yet know anything about a function named inc. Now issue the following directive to the toplevel:
发生错误是因为顶层还不知道有关名为 inc 的函数的任何信息。现在向顶层发出以下指令:

# #use "mycode.ml";;

Note that the first # character above indicates the toplevel prompt to you. The second # character is one that you type to tell the toplevel that you are issuing a directive. Without that character, the toplevel would think that you are trying to apply a function named use.
请注意,上面的第一个 # 字符向您指示顶级提示。第二个 # 字符是您键入的一个字符,用于告诉顶层您正在发出指令。如果没有该字符,顶层会认为您正在尝试应用名为 use 的函数。

Now try again: 现在再试一次:

inc 3
- : int = 4

2.1.4. Workflow in the toplevel
2.1.4. 顶层工作流程 ¶

The best workflow when using the toplevel with code stored in files is:
将顶层与存储在文件中的代码一起使用时的最佳工作流程是:

  • Edit the code in the file.
    编辑文件中的代码。

  • Load the code in the toplevel with #use.
    使用 #use 在顶层加载代码。

  • Interactively test the code.
    交互式测试代码。

  • Exit the toplevel. Warning: do not skip this step.
    退出顶层。警告:不要跳过此步骤。

Tip

Suppose you wanted to fix a bug in your code. It’s tempting to not exit the toplevel, edit the file, and re-issue the #use directive into the same toplevel session. Resist that temptation. The “stale code” that was loaded from an earlier #use directive in the same session can cause surprising things to happen—surprising when you’re first learning the language, anyway. So always exit the toplevel before re-using a file.
假设您想修复代码中的错误。人们很容易不退出顶层,编辑文件,然后将 #use 指令重新发出到同一顶层会话中。抵制这种诱惑。从同一会话中较早的 #use 指令加载的“过时代码”可能会导致令人惊讶的事情发生 - 无论如何,当您第一次学习该语言时,这会令人惊讶。因此,在重新使用文件之前,请务必退出顶层。

2.2. Compiling OCaml Programs
2.2. 编译 OCaml 程序 ¶

Using OCaml as a kind of interactive calculator can be fun, but we won’t get very far with writing large programs that way. We instead need to store code in files and compile them.
使用 OCaml 作为一种交互式计算器可能很有趣,但我们用这种方式编写大型程序不会走得太远。相反,我们需要将代码存储在文件中并编译它们。

2.2.1. Storing code in files
2.2.1. 在文件中存储代码 ¶

Open a terminal, create a new directory, and open VS Code in that directory. For example, you could use the following commands:
打开终端,创建一个新目录,然后在该目录中打开 VS Code。例如,您可以使用以下命令:

$ mkdir hello-world
$ cd hello-world

Warning 警告

Do not use the root of your Unix home directory as the place you store the file. The build system we are going to use very soon, dune, might not work right in the root of your home directory. Instead, you need to use a subdirectory of your home directory.
不要使用 Unix 主目录的根目录作为存储文件的位置。我们即将使用的构建系统,dune,可能无法在您的主目录的根目录中正常工作。相反,您需要使用主目录的子目录。

Use VS Code to create a new file named hello.ml. Enter the following code into the file:
使用 VS Code 创建一个名为 hello.ml 的新文件。在文件中输入以下代码:

let _ = print_endline "Hello world!"

Note 笔记

There is no double semicolon ;; at the end of that line of code. The double semicolon is intended for interactive sessions in the toplevel, so that the toplevel knows you are done entering a piece of code. There’s usually no reason to write it in a .ml file.
该行代码末尾没有双分号 ;; 。双分号用于顶层的交互式会话,以便顶层知道您已完成输入一段代码。通常没有理由将其写入 .ml 文件。

The let _ = above means that we don’t care to give a name (hence the “blank” or underscore) to code on the right-hand side of the =.
上面的 let _ = 意味着我们不关心为 = 右侧的代码命名(因此是“空白”或下划线)。

Save the file and return to the command line. Compile the code:
保存文件并返回到命令行。编译代码:

$ ocamlc -o hello.byte hello.ml

The compiler is named ocamlc. The -o hello.byte option says to name the output executable hello.byte. The executable contains compiled OCaml bytecode. In addition, two other files are produced, hello.cmi and hello.cmo. We don’t need to be concerned with those files for now. Run the executable:
编译器名为 ocamlc-o hello.byte 选项表示将输出可执行文件命名为 hello.byte 。可执行文件包含已编译的 OCaml 字节码。此外,还会生成另外两个文件: hello.cmihello.cmo 。我们暂时不需要关心这些文件。运行可执行文件:

$ ./hello.byte

It should print Hello world! and terminate.
它应该打印 Hello world! 并终止。

Now change the string that is printed to something of your choice. Save the file, recompile, and rerun. Try making the code print multiple lines.
现在将打印的字符串更改为您选择的字符串。保存文件,重新编译并重新运行。尝试让代码打印多行。

This edit-compile-run cycle between the editor and the command line is something that might feel unfamiliar if you’re used to working inside IDEs like Eclipse. Don’t worry; it will soon become second nature.
如果您习惯在 Eclipse 等 IDE 中工作,那么编辑器和命令行之间的编辑-编译-运行循环可能会让您感到陌生。不用担心;它很快就会成为第二天性。

Now let’s clean up all those generated files:
现在让我们清理所有生成的文件:

$ rm hello.byte hello.cmi hello.cmo

2.2.2. What about Main?
2.2.2. 主要呢? ¶

Unlike C or Java, OCaml programs do not need to have a special function named main that is invoked to start the program. The usual idiom is just to have the very last definition in a file serve as the main function that kicks off whatever computation is to be done.
与 C 或 Java 不同,OCaml 程序不需要调用名为 main 的特殊函数来启动程序。通常的习惯用法是将文件中的最后一个定义作为主函数,启动要完成的任何计算。

2.2.3. Dune 2.2.3. 沙丘 ¶

In larger projects, we don’t want to run the compiler or clean up manually. Instead, we want to use a build system to automatically find and link in libraries. OCaml has a legacy build system called ocamlbuild, and a newer build system called Dune. Similar systems include make, which has long been used in the Unix world for C and other languages; and Gradle, Maven, and Ant, which are used with Java.
在较大的项目中,我们不想运行编译器或手动清理。相反,我们希望使用构建系统来自动查找和链接库。 OCaml 有一个名为 ocamlbuild 的旧构建系统和一个名为 Dune 的较新构建系统。类似的系统包括 make ,它长期以来在 Unix 世界中用于 C 和其他语言;以及与 Java 一起使用的 Gradle、Maven 和 Ant。

A Dune project is a directory (and its subdirectories) that contain OCaml code you want to compile. The root of a project is the highest directory in its hierarchy. A project might rely on external packages providing additional code that is already compiled. Usually, packages are installed with OPAM, the OCaml Package Manager.
Dune 项目是一个目录(及其子目录),其中包含要编译的 OCaml 代码。项目的根目录是其层次结构中的最高目录。项目可能依赖于提供已编译的附加代码的外部包。通常,包是通过 OPAM(OCaml 包管理器)安装的。

Each directory in your project can contain a file named dune. That file describes to Dune how you want the code in that directory (and subdirectories) to be compiled. Dune files use a functional-programming syntax descended from LISP called s-expressions, in which parentheses are used to show nested data that form a tree, much like HTML tags do. The syntax of Dune files is documented in the Dune manual.
项目中的每个目录都可以包含一个名为 dune 的文件。该文件向 Dune 描述了您希望如何编译该目录(和子目录)中的代码。 Dune 文件使用源自 LISP 的函数编程语法(称为 s-表达式),其中括号用于显示形成树的嵌套数据,就像 HTML 标签一样。 Dune 文件的语法记录在 Dune 手册中。

Here is a small example of how to use Dune. In the same directory as hello.ml, create a file named dune and put the following in it:
这是一个如何使用 Dune 的小示例。在与 hello.ml 相同的目录中,创建一个名为 dune 的文件,并将以下内容放入其中:

(executable
 (name hello))

That declares an executable (a program that can be executed) whose main file is hello.ml.
它声明了一个可执行文件(可以执行的程序),其主文件是 hello.ml

Also create a file named dune-project and put the following in it:
另外创建一个名为 dune-project 的文件并在其中放入以下内容:

(lang dune 3.4)

That tells Dune that this project uses Dune version 3.4, which was current at the time this version of the textbook was released. This  project 项目 file is needed in the root directory of every source tree that you want to compile with Dune. In general, you’ll have a
您想要使用 Dune 编译的每个源代码树的根目录中都需要文件。一般来说,您将有一个
dune file in every subdirectory of the source tree but only one
文件位于源树的每个子目录中,但只有一个
dune-project file at the root.
文件在根目录下。

这告诉 Dune 该项目使用 Dune 3.4 版本,该版本在该版本的教科书发布时是最新的。您想要使用 Dune 编译的每个源代码树的根目录中都需要此项目文件。一般来说,源树的每个子目录中都会有一个 dune 文件,但根目录下只有一个 dune-project 文件。

Then run this command from the terminal:
然后从终端运行此命令:

$ dune build hello.exe

Note that the .exe extension is used on all platforms by Dune, not just on Windows. That causes Dune to build a native executable rather than a bytecode executable.
请注意, .exe 扩展名在 Dune 的所有平台上使用,而不仅仅是在 Windows 上。这会导致 Dune 构建本机可执行文件而不是字节码可执行文件。

Dune will create a directory _build and compile our program inside it. That’s one benefit of the build system over directly running the compiler: instead of polluting your source directory with a bunch of generated files, they get cleanly created in a separate directory. Inside _build there are many files that get created by Dune. Our executable is buried a couple of levels down:
Dune 将创建一个目录 _build 并在其中编译我们的程序。这是构建系统相对于直接运行编译器的好处之一:它们不会用一堆生成的文件污染源目录,而是在单独的目录中干净地创建它们。 _build 内部有许多由 Dune 创建的文件。我们的可执行文件被埋藏在下面几层:

$ _build/default/hello.exe
Hello world!

But Dune provides a shortcut to having to remember and type all of that. To build and execute the program in one step, we can simply run:
但《沙丘》提供了一条捷径,让您无需记住并输入所有这些内容。要一步构建并执行程序,我们只需运行:

$ dune exec ./hello.exe
Hello world!

Finally, to clean up all the compiled code we just run:
最后,为了清理我们刚刚运行的所有编译代码:

$ dune clean

That removes the _build directory, leaving just your source code.
这将删除 _build 目录,只留下源代码。

Tip

When Dune compiles your program, it caches a copy of your source files in _build/default. If you ever accidentally make a mistake that results in loss of a source file, you might be able to recover it from inside _build. Of course, using source control like git is also advisable.
当 Dune 编译您的程序时,它会在 _build/default 中缓存源​​文件的副本。如果您不小心犯了一个错误,导致源文件丢失,您也许可以从 _build 内部恢复它。当然,使用像 git 这样的源代码管理也是可取的。

2.3. Expressions 2.3. 表达式 ¶

The primary piece of OCaml syntax is the expression. Just like programs in imperative languages are primarily built out of commands, programs in functional languages are primarily built out of expressions. Examples of expressions include 2+2 and increment 21.
OCaml 语法的主要部分是表达式。就像命令式语言中的程序主要由命令构建一样,函数式语言中的程序主要由表达式构建。表达式的示例包括 2+2increment 21

The OCaml manual has a complete definition of all the expressions in the language. Though that page starts with a rather cryptic overview, if you scroll down, you’ll come to some English explanations. Don’t worry about studying that page now; just know that it’s available for reference.
OCaml 手册对该语言中的所有表达式都有完整的定义。尽管该页面以相当神秘的概述开头,但如果向下滚动,您会看到一些英文解释。现在不用担心学习该页面;只是知道它可供参考。

The primary task of computation in a functional language is to evaluate an expression to a value. A value is an expression for which there is no computation remaining to be performed. So, all values are expressions, but not all expressions are values. Examples of values include 2, true, and "yay!".
函数式语言中计算的主要任务是将表达式计算为值。值是没有剩余计算需要执行的表达式。因此,所有值都是表达式,但并非所有表达式都是值。值的示例包括 2true"yay!"

The OCaml manual also has a definition of all the values, though again, that page is mostly useful for reference rather than study.
OCaml 手册还提供了所有值的定义,不过该页面主要用于参考而不是研究。

Sometimes an expression might fail to evaluate to a value. There are two reasons that might happen:
有时表达式可能无法计算出某个值。可能发生的原因有两个:

  1. Evaluation of the expression raises an exception.
    表达式的求值引发异常。

  2. Evaluation of the expression never terminates (e.g., it enters an “infinite loop”).
    表达式的求值永远不会终止(例如,它进入“无限循环”)。

2.3.1. Primitive Types and Values
2.3.1. 原始类型和值 ¶

The primitive types are the built-in and most basic types: integers, floating-point numbers, characters, strings, and booleans. They will be recognizable as similar to primitive types from other programming languages.
基本类型是内置的最基本类型:整数、浮点数、字符、字符串和布尔值。它们将被识别为与其他编程语言中的原始类型类似。

Type int: Integers. OCaml integers are written as usual: 1, 2, etc. The usual operators are available: +, -, *, /, and mod. The latter two are integer division and modulus:
类型 int :整数。 OCaml 整数的书写方式与往常一样: 12 等。常用运算符可用: +-*/mod 。后两者是整数除法和模数:

65 / 60
- : int = 1
65 mod 60
- : int = 5
65 / 0
Exception: Division_by_zero.
Raised by primitive operation at unknown location
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

OCaml integers range from 262 to 2621 on modern platforms. They are implemented with 64-bit machine words, which is the size of a register on 64-bit processor. But one of those bits is “stolen” by the OCaml implementation, leading to a 63-bit representation. That bit is used at run time to distinguish integers from pointers. For applications that need true 64-bit integers, there is an Int64 module in the standard library. And for applications that need arbitrary-precision integers, there is a separate Zarith library. But for most purposes, the built-in int type suffices and offers the best performance.
在现代平台上,OCaml 整数的范围是从 2622621 。它们是用 64 位机器字实现的,这是 64 位处理器上寄存器的大小。但其中一位被 OCaml 实现“窃取”,从而产生 63 位表示。该位在运行时用于区分整数和指针。对于需要真正 64 位整数的应用程序,标准库中有一个 Int64 模块。对于需要任意精度整数的应用程序,有一个单独的 Zarith 库。但对于大多数用途,内置的 int 类型就足够了,并提供最佳性能。

Type float: Floating-point numbers. OCaml floats are IEEE 754 double-precision floating-point numbers. Syntactically, they must always contain a dot—for example, 3.14 or 3.0 or even 3.. The last is a float; if you write it as 3, it is instead an int:
类型 float :浮点数。 OCaml 浮点数是 IEEE 754 双精度浮点数。从语法上讲,它们必须始终包含一个点,例如 3.143.0 甚至 3. 。最后一个是 float ;如果你把它写成 3 ,它就会变成 int

3.
- : float = 3.
3
- : int = 3

OCaml deliberately does not support operator overloading, Arithmetic operations on floats are written with a dot after them. For example, floating-point multiplication is written *. not *:
OCaml 故意不支持运算符重载,浮点上的算术运算在其后写有一个点。例如,浮点乘法写作 *. 而不是 *

3.14 *. 2.
- : float = 6.28
3.14 * 2.
File "[7]", line 1, characters 0-4:
1 | 3.14 * 2.
    ^^^^
Error: This expression has type float but an expression was expected of type
         int

OCaml will not automatically convert between int and float. If you want to convert, there are two built-in functions for that purpose: int_of_float and float_of_int.
OCaml 不会自动在 intfloat 之间进行转换。如果您想进行转换,有两个内置函数可用于此目的: int_of_floatfloat_of_int

3.14 *. (float_of_int 2)
- : float = 6.28

As in any language, the floating-point representation is approximate. That can lead to rounding errors:
与任何语言一样,浮点表示是近似的。这可能会导致舍入错误:

0.1 +. 0.2
- : float = 0.300000000000000044

The same behavior can be observed in Python and Java, too. If you haven’t encountered this phenomenon before, here’s a basic guide to floating-point representation that you might enjoy reading.
在 Python 和 Java 中也可以观察到相同的行为。如果您以前没有遇到过这种现象,那么您可能会喜欢阅读以下浮点表示的基本指南。

Type bool: Booleans. The boolean values are written true and false. The usual short-circuit conjunction && and disjunction || operators are available.
类型 bool :布尔值。布尔值写作 truefalse 。可以使用常用的短路合取 && 和析取 || 运算符。

Type char: Characters. Characters are written with single quotes, such as 'a', 'b', and 'c'. They are represented as bytes —that is, 8-bit integers— in the ISO 8859-1 ISO 8859-1 “Latin-1” encoding. The first half of the characters in that range are the standard ASCII characters. You can convert characters to and from integers with char_of_int and int_of_char.
类型 char :字符。字符用单引号书写,例如 'a''b''c' 。它们在 ISO 8859-1“Latin-1”编码中表示为字节(即 8 位整数)。该范围内的前半部分字符是标准 ASCII 字符。您可以使用 char_of_intint_of_char 将字符与整数相互转换。

Type string: Strings. Strings are sequences of characters. They are written with double quotes, such as "abc". The string concatenation operator is ^:
类型 string :字符串。字符串是字符序列。它们用双引号书写,例如 "abc" 。字符串连接运算符是 ^

"abc" ^ "def"
- : string = "abcdef"

Object-oriented languages often provide an overridable method for converting objects to strings, such as toString() in Java or __str__() in Python. But most OCaml values are not objects, so another means is required to convert to strings. For three of the primitive types, there are built-in functions: string_of_int, string_of_float, string_of_bool. Strangely, there is no string_of_char, but the library function String.make can be used to accomplish the same goal.
面向对象的语言通常提供可重写的方法来将对象转换为字符串,例如 Java 中的 toString() 或 Python 中的 __str__() 。但大多数 OCaml 值都不是对象,因此需要另一种方法来转换为字符串。对于三种基本类型,有内置函数: string_of_intstring_of_floatstring_of_bool 。奇怪的是,没有 string_of_char ,但是可以使用库函数 String.make 来完成相同的目标。

string_of_int 42
- : string = "42"
String.make 1 'z'
- : string = "z"

Likewise, for the same three primitive types, there are built-in functions to convert from a string if possible: int_of_string, float_of_string, and bool_of_string.
同样,对于相同的三种基元类型,如果可能的话,有内置函数可以从字符串进行转换: int_of_stringfloat_of_stringbool_of_string

int_of_string "123"
- : int = 123
int_of_string "not an int"
Exception: Failure "int_of_string".
Raised by primitive operation at unknown location
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

There is no char_of_string, but the individual characters of a string can be accessed by a 0-based index. The indexing operator is written with a dot and square brackets:
没有 char_of_string ,但可以通过基于 0 的索引来访问字符串的各个字符。索引运算符用点和方括号编写:

"abc".[0]
- : char = 'a'
"abc".[1]
- : char = 'b'
"abc".[3]
Exception: Invalid_argument "index out of bounds".
Raised by primitive operation at unknown location
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

2.3.2. More Operators 2.3.2. 更多运算符 ¶

We’ve covered most of the built-in operators above, but there are a few more that you can see in the OCaml manual.
我们已经介绍了上面的大部分内置运算符,但您还可以在 OCaml 手册中看到更多内容。

There are two equality operators in OCaml, = and ==, with corresponding inequality operators <> and !=. Operators = and <> examine structural equality whereas == and != examine physical equality. Until we’ve studied the imperative features of OCaml, the difference between them will be tricky to explain. See the documentation of Stdlib.(==) if you’re curious now.
OCaml 中有两个相等运算符 === ,以及相应的不等运算符 <>!= 。运算符 =<> 检查结构相等性,而 ==!= 检查物理相等性。在我们研究 OCaml 的命令式功能之前,它们之间的差异很难解释。如果您现在好奇,请参阅 Stdlib.(==) 的文档。

Important 重要的

Start training yourself now to use = and not to use ==. This will be difficult if you’re coming from a language like Java where == is the usual equality operator.
现在开始训练自己使用 = 而不是使用 == 。如果您来自像 Java 这样的语言,其中 == 是通常的相等运算符,那么这将很困难。

2.3.3. Assertions 2.3.3. 断言 ¶

The expression assert e evaluates e. If the result is true, nothing more happens, and the entire expression evaluates to a special value called unit. The unit value is written () and its type is unit. But if the result is false, an exception is raised.
表达式 assert e 计算 e 。如果结果是 true ,则不会再发生任何事情,并且整个表达式的计算结果为一个称为单位的特殊值。单位值写作 () ,其类型为 unit 。但如果结果是 false ,则会引发异常。

2.3.4. If Expressions 2.3.4. If 表达式 ¶

The expression if e1 then e2 else e3 evaluates to e2 if e1 evaluates to true, and to e3 otherwise. We call e1 the guard of the if expression.
如果 e1 计算结果为 true ,则表达式 if e1 then e2 else e3 计算结果为 e2 ,否则计算为 e3 。我们将 e1 称为 if 表达式的守卫。

if 3 + 5 > 2 then "yay!" else "boo!"
- : string = "yay!"

Unlike if-then-else statements that you may have used in imperative languages, if-then-else expressions in OCaml are just like any other expression; they can be put anywhere an expression can go. That makes them similar to the ternary operator ? : that you might have used in other languages.
与您在命令式语言中使用的 if-then-else 语句不同,OCaml 中的 if-then-else 表达式就像任何其他表达式一样;它们可以放在表达式可以到达的任何地方。这使得它们类似于您可能在其他语言中使用的三元运算符 ? :

4 + (if 'a' = 'b' then 1 else 2)
- : int = 6

If expressions can be nested in a pleasant way:
If 表达式可以以一种愉快的方式嵌套:

if e1 then e2
else if e3 then e4
else if e5 then e6
...
else en

You should regard the final else as mandatory, regardless of whether you are writing a single if expression or a highly nested if expression. If you omit it you’ll likely get an error message that, for now, is inscrutable:
无论您是编写单个 if 表达式还是高度嵌套的 if 表达式,您都应该将最终的 else 视为强制性的。如果省略它,您可能会收到一条目前难以理解的错误消息:

if 2 > 3 then 5
File "[20]", line 1, characters 14-15:
1 | if 2 > 3 then 5
                  ^
Error: This expression has type int but an expression was expected of type
         unit
       because it is in the result of a conditional with no else branch

Syntax. The syntax of an if expression:
句法。 if 表达式的语法:

if e1 then e2 else e3

The letter e is used here to represent any other OCaml expression; it’s an example of a syntactic variable aka metavariable, which is not actually a variable in the OCaml language itself, but instead a name for a certain syntactic construct. The numbers after the letter e are being used to distinguish the three different occurrences of it.
此处使用字母 e 来表示任何其他 OCaml 表达式;这是一个语法变量(也称为元变量)的示例,它实际上并不是 OCaml 语言本身的变量,而是某个语法结构的名称。字母 e 后面的数字用于区分它的三个不同出现。

Dynamic semantics. The dynamic semantics of an if expression:
动态语义。 if 表达式的动态语义:

  • If e1 evaluates to true, and if e2 evaluates to a value v, then if e1 then e2 else e3 evaluates to v
    如果 e1 计算结果为 true ,并且 e2 计算结果为值 v ,则 if e1 then e2 else e3 计算结果为 v

  • If e1 evaluates to false, and if e3 evaluates to a value v, then if e1 then e2 else e3 evaluates to v.
    如果 e1 计算结果为 false ,并且 e3 计算结果为值 v ,则 if e1 then e2 else e3 计算结果为 v

We call these evaluation rules: they define how to evaluate expressions. Note how it takes two rules to describe the evaluation of an if expression, one for when the guard is true, and one for when the guard is false. The letter v is used here to represent any OCaml value; it’s another example of a metavariable. Later we will develop a more mathematical way of expressing dynamic semantics, but for now we’ll stick with this more informal style of explanation.
我们称这些评估规则为:它们定义如何评估表达式。请注意,如何使用两条规则来描述 if 表达式的求值,一条用于当防护为 true 时,另一条用于当防护为 false 时。此处使用字母 v 来表示任何 OCaml 值;这是元变量的另一个例子。稍后我们将开发一种更数学的方式来表达动态语义,但现在我们将坚持这种更非正式的解释风格。

Static semantics. The static semantics of an if expression:
静态语义。 if 表达式的静态语义:

  • If e1 has type bool and e2 has type t and e3 has type t then if e1 then e2 else e3 has type t
    如果 e1 具有类型 boole2 具有类型 te3 具有类型 t 那么 if e1 then e2 else e3 的类型为 t

We call this a typing rule: it describes how to type check an expression. Note how it only takes one rule to describe the type checking of an if expression. At compile time, when type checking is done, it makes no difference whether the guard is true or false; in fact, there’s no way for the compiler to know what value the guard will have at run time. The letter t here is used to represent any OCaml type; the OCaml manual also has definition of all types (which curiously does not name the base types of the language like int and bool).
我们称其为键入规则:它描述了如何键入检查表达式。请注意,如何只需要一条规则来描述 if 表达式的类型检查。在编译时,当进行类型检查时,守卫是 true 或 false 都没有区别;事实上,编译器无法知道守卫在运行时将具有什么值。这里的字母 t 用于表示任何OCaml类型; OCaml 手册还定义了所有类型(奇怪的是,它没有命名语言的基本类型,如 intbool )。

We’re going to be writing “has type” a lot, so let’s introduce a more compact notation for it. Whenever we would write “e has type t”, let’s instead write e : t. The colon is pronounced “has type”. This usage of colon is consistent with how the toplevel responds after it evaluates an expression that you enter:
我们将经常写“has type”,所以让我们为它引入一个更紧凑的表示法。每当我们写“ e has type t ”时,我们就写 e : t 。冒号发音为“has type”。冒号的这种用法与顶层在计算您输入的表达式后的响应方式一致:

let x = 42
val x : int = 42

In the above example, variable x has type int, which is what the colon indicates.
在上面的示例中,变量 x 的类型为 int ,这就是冒号所指示的类型。

2.3.5. Let Expressions 2.3.5. Let 表达式 ¶

In our use of the word let thus far, we’ve been making definitions in the toplevel and in .ml files. For example,
到目前为止,在我们使用单词 let 时,我们一直在顶层和 .ml 文件中进行定义。例如,

let x = 42;;
val x : int = 42

defines x to be 42, after which we can use x in future definitions at the toplevel. We’ll call this use of let a let definition.
x 定义为 42,之后我们可以在将来的顶层定义中使用 x 。我们将 let 的这种使用称为 let 定义。

There’s another use of let which is as an expression:
let 还有另一种用法,即作为表达式:

let x = 42 in x + 1
- : int = 43

Here we’re binding a value to the name x then using that binding inside another expression, x+1. We’ll call this use of let a let expression. Since it’s an expression it evaluates to a value. That’s different than definitions, which themselves do not evaluate to any value. You can see that if you try putting a let definition in place of where an expression is expected:
在这里,我们将一个值绑定到名称 x ,然后在另一个表达式 x+1 中使用该绑定。我们将 let 的这种用法称为 let 表达式。因为它是一个表达式,所以它的计算结果是一个值。这与定义不同,定义本身不会评估任何值。您可以看到,如果尝试将 let 定义放在需要表达式的位置:

(let x = 42) + 1
File "[24]", line 1, characters 11-12:
1 | (let x = 42) + 1
               ^
Error: Syntax error

Syntactically, a let definition is not permitted on the left-hand side of the + operator, because a value is needed there, and definitions do not evaluate to values. On the other hand, a let expression would work fine:
从语法上讲, let 定义不允许出现在 + 运算符的左侧,因为那里需要一个值,并且定义不会计算为值。另一方面, let 表达式可以正常工作:

(let x = 42 in x) + 1
- : int = 43

Another way to understand let definitions at the toplevel is that they are like let expression where we just haven’t provided the body expression yet. Implicitly, that body expression is whatever else we type in the future. For example,
理解顶层的 let 定义的另一种方法是,它们就像 let 表达式,只是我们还没有提供主体表达式。隐含地,该身​​体表情就是我们将来键入的任何其他内容。例如,

# let a = "big";;
# let b = "red";;
# let c = a ^ b;;
# ...

is understand by OCaml in the same way as
OCaml 的理解方式与

let a = "big" in
let b = "red" in
let c = a ^ b in
...

That latter series of let bindings is idiomatically how several variables can be bound inside a given block of code.
后一系列 let 绑定是惯用的方式,可以将多个变量绑定到给定的代码块内。

Syntax.

let x = e1 in e2

As usual, x is an identifier. These identifiers must begin with lower-case, not upper, and idiomatically are written with snake_case not camelCase. We call e1 the binding expression, because it’s what’s being bound to x; and we call e2 the body expression, because that’s the body of code in which the binding will be in scope.
与往常一样, x 是一个标识符。这些标识符必须以小写字母开头,而不是大写字母,并且通常用 snake_case 而不是 camelCase 编写。我们将 e1 称为绑定表达式,因为它是绑定到 x 的内容;我们将 e2 称为主体表达式,因为这是绑定范围内的代码主体。

Dynamic semantics. 动态语义。

To evaluate let x = e1 in e2: 评估 let x = e1 in e2

  • Evaluate e1 to a value v1.
    e1 计算为值 v1

  • Substitute v1 for x in e2, yielding a new expression e2'.
    e2 中的 x 替换为 v1 ,产生新的表达式 e2'

  • Evaluate e2' to a value v2.
    e2' 计算为值 v2

  • The result of evaluating the let expression is v2.
    let 表达式的计算结果是 v2

Here’s an example: 这是一个例子:

    let x = 1 + 4 in x * 3
-->   (evaluate e1 to a value v1)
    let x = 5 in x * 3
-->   (substitute v1 for x in e2, yielding e2')
    5 * 3
-->   (evaluate e2' to v2)
    15
      (result of evaluation is v2)

Static semantics. 静态语义。

  • If e1 : t1 and if under the assumption that x : t1 it holds that e2 : t2, then (let x = e1 in e2) : t2.
    如果 e1 : t1 并且在假设 x : t1 的情况下它保持 e2 : t2 ,则 (let x = e1 in e2) : t2

We use the parentheses above just for clarity. As usual, the compiler’s type inferencer determines what the type of the variable is, or the programmer could explicitly annotate it with this syntax:
我们使用上面的括号只是为了清楚起见。像往常一样,编译器的类型推断器确定变量的类型,或者程序员可以使用以下语法显式注释它:

let x : t = e1 in e2

2.3.6. Scope 2.3.6. 范围 ¶

Let bindings are in effect only in the block of code in which they occur. This is exactly what you’re used to from nearly any modern programming language. For example:
Let 绑定仅在它们出现的代码块中有效。这正是您在几乎所有现代编程语言中所习惯的。例如:

let x = 42 in
  (* y is not meaningful here *)
  x + (let y = "3110" in
         (* y is meaningful here *)
         int_of_string y)

The scope of a variable is where its name is meaningful. Variable y is in scope only inside of the let expression that binds it above.
变量的作用域是其名称有意义的地方。变量 y 的作用域仅位于上面绑定它的 let 表达式内部。

It’s possible to have overlapping bindings of the same name. For example:
可能存在同名的重叠绑定。例如:

let x = 5 in
  ((let x = 6 in x) + x)

But this is darn confusing, and for that reason, it is strongly discouraged style—much like ambiguous pronouns are discouraged in natural language. Nonetheless, let’s consider what that code means.
但这实在令人困惑,因此,这是一种强烈不鼓励的风格——就像自然语言中不鼓励使用歧义代词一样。尽管如此,让我们考虑一下该代码的含义。

To what value does that code evaluate? The answer comes down to how x is replaced by a value each time it occurs. Here are a few possibilities for such substitution:
该代码计算出什么值?答案归结为 x 每次出现时如何被一个值替换。以下是这种替代的几种可能性:

(* possibility 1 *)
let x = 5 in
  ((let x = 6 in 6) + 5)

(* possibility 2 *)
let x = 5 in
  ((let x = 6 in 5) + 5)

(* possibility 3 *)
let x = 5 in
  ((let x = 6 in 6) + 6)

The first one is what nearly any reasonable language would do. And most likely it’s what you would guess But, why?
第一个是几乎任何合理的语言都会做的事情。最有可能的是你会猜到但是,为什么呢?

The answer is something we’ll call the Principle of Name Irrelevance: the name of a variable shouldn’t intrinsically matter. You’re used to this from math. For example, the following two functions are the same:
答案就是我们所说的名称无关原则:变量的名称本质上不应该重要。你在数学上已经习惯了这一点。例如,以下两个函数是相同的:

f(x)=x2f(y)=y2

It doesn’t intrinsically matter whether we call the argument to the function x or y; either way, it’s still the squaring function. Therefore, in programs, these two functions should be identical:
我们是否调用函数的参数 x 还是 y 本质上并不重要;不管怎样,它仍然是平方函数。因此,在程序中,这两个函数应该是相同的:

let f x = x * x
let f y = y * y

This principle is more commonly known as alpha equivalence: the two functions are equivalent up to renaming of variables, which is also called alpha conversion for historical reasons that are unimportant here.
这一原则通常被称为 alpha 等价:两个函数在变量重命名之前是等价的,由于历史原因(这里不重要),这也称为 alpha 转换。

According to the Principle of Name Irrelevance, these two expressions should be identical:
根据名称无关原则,这两个表达式应该是相同的:

let x = 6 in x
let y = 6 in y

Therefore, the following two expressions, which have the above expressions embedded in them, should also be identical:
因此,嵌入了上述表达式的以下两个表达式也应该是相同的:

let x = 5 in (let x = 6 in x) + x
let x = 5 in (let y = 6 in y) + x

But for those to be identical, we must choose the first of the three possibilities above. It is the only one that makes the name of the variable be irrelevant.
但为了使它们相同,我们必须选择上述三种可能性中的第一种。这是唯一一个使变量名称变得无关紧要的。

There is a term commonly used for this phenomenon: a new binding of a variable shadows any old binding of the variable name. Metaphorically, it’s as if the new binding temporarily casts a shadow over the old binding. But eventually the old binding could reappear as the shadow recedes.
有一个术语通常用于描述这种现象:变量的新绑定会隐藏变量名称的任何旧绑定。打个比方,就好像新绑定暂时在旧绑定上投下了阴影。但最终,随着阴影消退,旧的绑定可能会重新出现。

Shadowing is not mutable assignment.
阴影不是可变分配。
For example, both of the following expressions evaluate to 11:

let x = 5 in ((let x = 6 in x) + x)
let x = 5 in (x + (let x = 6 in x))

Likewise, the following utop transcript is not mutable assignment, though at first it could seem like it is:
同样,以下 utop 转录本不是可变赋值,尽管乍一看似乎是这样:

# let x = 42;;
val x : int = 42
# let x = 22;;
val x : int = 22

Recall that every let definition in the toplevel is effectively a nested let expression. So the above is effectively the following:
回想一下,顶层中的每个 let 定义实际上都是一个嵌套的 let 表达式。所以上面的内容实际上如下:

let x = 42 in
  let x = 22 in
    ... (* whatever else is typed in the toplevel *)

The right way to think about this is that the second let binds an entirely new variable that just happens to have the same name as the first let.
正确的思考方式是,第二个 let 绑定一个全新的变量,该变量恰好与第一个 let 具有相同的名称。

Here is another utop transcript that is well worth studying:
这是另一个非常值得研究的 utop 成绩单:

# let x = 42;;
val x : int = 42
# let f y = x + y;;
val f : int -> int = <fun>
# f 0;;
: int = 42
# let x = 22;;
val x : int = 22
# f 0;;
- : int = 42  (* x did not mutate! *)

To summarize, each let definition binds an entirely new variable. If that new variable happens to have the same name as an old variable, the new variable temporarily shadows the old one. But the old variable is still around, and its value is immutable: it never, ever changes. So even though let expressions might superficially look like assignment statements from imperative languages, they are actually quite different.
总而言之,每个 let 定义都绑定一个全新的变量。如果新变量碰巧与旧变量同名,则新变量会暂时隐藏旧变量。但旧变量仍然存在,并且它的值是不可变的:它永远不会改变。因此,尽管 let 表达式表面上看起来像命令式语言中的赋值语句,但它们实际上是完全不同的。

2.3.7. Type Annotations
2.3.7. 类型注释 ¶

OCaml automatically infers the type of every expression, with no need for the programmer to write it manually. Nonetheless, it can sometimes be useful to manually specify the desired type of an expression. A type annotation does that:
OCaml 自动推断每个表达式的类型,无需程序员手动编写。尽管如此,有时手动指定所需的表达式类型还是很有用的。类型注释的作用是:

(5 : int)
- : int = 5

An incorrect annotation will produce a compile-time error:
不正确的注释将产生编译时错误:

(5 : float)
File "[27]", line 1, characters 1-2:
1 | (5 : float)
     ^
Error: This expression has type int but an expression was expected of type
         float
  Hint: Did you mean `5.'?

And that example shows why you might use manual type annotations during debugging. Perhaps you had forgotten that 5 cannot be treated as a float, and you tried to write:
该示例说明了为什么您可以在调试期间使用手动类型注释。也许您忘记了 5 不能被视为 float ,并且您尝试编写:

5 +. 1.1

You might try manually specifying that 5 was supposed to be a float:
您可以尝试手动指定 5 应该是 float

(5 : float) +. 1.1
File "[28]", line 1, characters 1-2:
1 | (5 : float) +. 1.1
     ^
Error: This expression has type int but an expression was expected of type
         float
  Hint: Did you mean `5.'?

It’s clear that the type annotation has failed. Although that might seem silly for this tiny program, you might find this technique to be effective as programs get larger.
很明显,类型注释失败了。尽管这对于这个小程序来说可能看起来很愚蠢,但随着程序变得越来越大,您可能会发现这种技术很有效。

Important 重要的

Type annotations are not type casts, such as might be found in C or Java. They do not indicate a conversion from one type to another. Rather they indicate a check that the expression really does have the given type.
类型注释不是类型转换,例如 C 或 Java 中可能存在的类型转换。它们并不表示从一种类型到另一种类型的转换。相反,它们指示检查表达式是否确实具有给定类型。

Syntax. The syntax of a type annotation:
句法。类型注释的语法:

(e : t)

Note that the parentheses are required.
请注意,括号是必需的。

Dynamic semantics. There is no run-time meaning for a type annotation. It goes away during compilation, because it indicates a compile-time check. There is no run-time conversion. So, if (e : t) compiled successfully, then at run-time it is simply e, and it evaluates as e would.
动态语义。类型注释没有运行时意义。它在编译期间消失,因为它指示编译时检查。没有运行时转换。因此,如果 (e : t) 编译成功,那么在运行时它只是 e ,并且其计算结果与 e 相同。

Static semantics. If e has type t then (e : t) has type t.
静态语义。如果 e 具有类型 t ,则 (e : t) 具有类型 t

2.4. Functions 2.4. 函数 ¶

Since OCaml is a functional language, there’s a lot to cover about functions. Let’s get started.
由于 OCaml 是一种函数式语言,因此有很多关于函数的内容需要介绍。让我们开始吧。

Important 重要的

Methods and functions are not the same idea. A method is a component of an object, and it implicitly has a receiver that is usually accessed with a keyword like this or self. OCaml functions are not methods: they are not components of objects, and they do not have a receiver.
方法和函数不是同一个概念。方法是对象的一个​​组件,它隐式地具有一个通常使用 thisself 等关键字访问的接收器。 OCaml 函数不是方法:它们不是对象的组件,并且没有接收器。

Some might say that all methods are functions, but not all functions are methods. Some might even quibble with that, making a distinction between functions and procedures. The latter would be functions that do not return any meaningful value, such as a void return type in Java or None return value in Python.
有人可能会说所有方法都是函数,但并非所有函数都是方法。有些人甚至可能会对此提出质疑,区分函数和过程。后者是不返回任何有意义值的函数,例如 Java 中的 void 返回类型或 Python 中的 None 返回值。

So if you’re coming from an object-oriented background, be careful about the terminology. Everything here is a strictly a function, not a method.
因此,如果您有面向对象的背景,请注意术语。这里的一切都是严格的函数,而不是方法。

2.4.1. Function Definitions
2.4.1. 函数定义 ¶

The following code 下面的代码

let x = 42

has an expression in it (42) but is not itself an expression. Rather, it is a definition. Definitions bind values to names, in this case the value 42 being bound to the name x. The OCaml manual describes definitions (see the third major grouping titled “definition” on that page), but that manual page is again primarily for reference not for study. Definitions are not expressions, nor are expressions definitions—they are distinct syntactic classes.
其中有一个表达式 ( 42 ),但它本身不是一个表达式。相反,它是一个定义。定义将值绑定到名称,在本例中,值 42 绑定到名称 x 。 OCaml 手册描述了定义(请参阅该页面上标题为“定义”的第三个主要分组),但该手册页同样主要用于参考而不是用于研究。定义不是表达式,表达式定义也不是——它们是不同的语法类。

For now, let’s focus on one particular kind of definition, a function definition. Non-recursive functions are defined like this:
现在,让我们关注一种特定类型的定义,即函数定义。非递归函数的定义如下:

let f x = ...

Recursive functions are defined like this:
递归函数的定义如下:

let rec f x = ...

The difference is just the rec keyword. It’s probably a bit surprising that you explicitly have to add a keyword to make a function recursive, because most languages assume by default that they are. OCaml doesn’t make that assumption, though. (Nor does the Scheme family of languages.)
区别只是 rec 关键字。明确必须添加关键字才能使函数递归可能有点令人惊讶,因为大多数语言默认情况下都假设它们是递归的。但 OCaml 并没有做出这样的假设。 (Scheme 语言家族也没有。)

One of the best known recursive functions is the factorial function. In OCaml, it can be written as follows:
最著名的递归函数之一是阶乘函数。在OCaml中,可以写成如下:

(** [fact n] is [n]!.
    Requires: [n >= 0]. *)
let rec fact n = if n = 0 then 1 else n * fact (n - 1)
val fact : int -> int = <fun>

We provided a specification comment above the function to document the precondition (Requires) and postcondition (is) of the function.
我们在函数上方提供了规范注释来记录函数的前置条件 ( Requires ) 和后置条件 ( is )。

Note that, as in many languages, OCaml integers are not the “mathematical” integers but are limited to a fixed number of bits. The manual specifies that (signed) integers are at least 31 bits, but they could be wider. As architectures have grown, so has that size. In current implementations, OCaml integers are 63 bits. So if you test on large enough inputs, you might begin to see strange results. The problem is machine arithmetic, not OCaml. (For interested readers: why 31 or 63 instead of 32 or 64? The OCaml garbage collector needs to distinguish between integers and pointers. The runtime representation of these therefore steals one bit to flag whether a word is an integer or a pointer.)
请注意,与许多语言一样,OCaml 整数不是“数学”整数,而是限制为固定位数。手册规定(有符号)整数至少为 31 位,但它们可以更宽。随着架构的发展,其规模也随之扩大。在当前的实现中,OCaml 整数是 63 位。因此,如果您在足够大的输入上进行测试,您可能会开始看到奇怪的结果。问题是机器算术,而不是 OCaml。 (感兴趣的读者:为什么是 31 或 63,而不是 32 或 64?OCaml 垃圾收集器需要区分整数和指针。因此,它们的运行时表示会窃取一位来标记一个字是整数还是指针。)

Here’s another recursive function:
这是另一个递归函数:

(** [pow x y] is [x] to the power of [y].
     Requires: [y >= 0]. *)
let rec pow x y = if y = 0 then 1 else x * pow x (y - 1)
val pow : int -> int -> int = <fun>

Note how we didn’t have to write any types in either of our functions: the OCaml compiler infers them for us automatically. The compiler solves this type inference problem algorithmically, but we could do it ourselves, too. It’s like a mystery that can be solved by our mental power of deduction:
请注意,我们不必在任何一个函数中编写任何类型:OCaml 编译器会自动为我们推断它们。编译器通过算法解决了这种类型推断问题,但我们也可以自己做。这就像一个谜团,可以通过我们的精神力推演来解开:

  • Since the if expression can return 1 in the then branch, we know by the typing rule for if that the entire if expression has type int.
    由于 if 表达式可以在 then 分支中返回 1 ,因此通过 if 的键入规则我们知道整个 if 表达式的类型为 int

  • Since the if expression has type int, the function’s return type must be int.
    由于 if 表达式的类型为 int ,因此函数的返回类型必须为 int

  • Since y is compared to 0 with the equality operator, y must be an int.
    由于 y0 使用相等运算符进行比较,因此 y 必须是 int

  • Since x is multiplied with another expression using the * operator, x must be an int.
    由于 x 使用 * 运算符与另一个表达式相乘,因此 x 必须是 int

If we wanted to write down the types for some reason, we could do that:
如果我们出于某种原因想写下类型,我们可以这样做:

let rec pow (x : int) (y : int) : int = ...

The parentheses are mandatory when we write the type annotations for x and y. We will generally leave out these annotations, because it’s simpler to let the compiler infer them. There are other times when you’ll want to explicitly write down types. One particularly useful time is when you get a type error from the compiler that you don’t understand. Explicitly annotating the types can help with debugging such an error message.
当我们为 xy 编写类型注释时,括号是必需的。我们通常会省略这些注释,因为让编译器推断它们更简单。有时您会想要显式地写下类型。一个特别有用的时刻是当您从编译器中收到您不理解的类型错误时。显式注释类型可以帮助调试此类错误消息。

Syntax. The syntax for function definitions:
句法。函数定义的语法:

let rec f x1 x2 ... xn = e

The f is a metavariable indicating an identifier being used as a function name. These identifiers must begin with a lowercase letter. The remaining rules for lowercase identifiers can be found in the manual. The names x1 through xn are metavariables indicating argument identifiers. These follow the same rules as function identifiers. The keyword rec is required if f is to be a recursive function; otherwise it may be omitted.
f 是指示用作函数名称的标识符的元变量。这些标识符必须以小写字母开头。小写标识符的其余规则可以在手册中找到。名称 x1xn 是指示参数标识符的元变量。它们遵循与函数标识符相同的规则。如果 f 是递归函数,则需要关键字 rec ;否则可以省略。

Note that syntax for function definitions is actually simplified compared to what OCaml really allows. We will learn more about some augmented syntax for function definition in the next couple weeks. But for now, this simplified version will help us focus.
请注意,与 OCaml 真正允许的语法相比,函数定义的语法实际上得到了简化。在接下来的几周内,我们将详细了解函数定义的一些增强语法。但就目前而言,这个简化版本将帮助我们集中注意力。

Mutually recursive functions can be defined with the and keyword:
可以使用 and 关键字定义相互递归函数:

let rec f x1 ... xn = e1
and g y1 ... yn = e2

For example: 例如:

(** [even n] is whether [n] is even.
    Requires: [n >= 0]. *)
let rec even n =
  n = 0 || odd (n - 1)

(** [odd n] is whether [n] is odd.
    Requires: [n >= 0]. *)
and odd n =
  n <> 0 && even (n - 1);;
val even : int -> bool = <fun>
val odd : int -> bool = <fun>

The syntax for function types is:
函数类型的语法是:

t -> u
t1 -> t2 -> u
t1 -> ... -> tn -> u

The t and u are metavariables indicating types. Type t -> u is the type of a function that takes an input of type t and returns an output of type u. We can think of t1 -> t2 -> u as the type of a function that takes two inputs, the first of type t1 and the second of type t2, and returns an output of type u. Likewise for a function that takes n arguments.
tu 是指示类型的元变量。类型 t -> u 是接受 t 类型输入并返回 u 类型输出的函数类型。我们可以将 t1 -> t2 -> u 视为接受两个输入的函数类型,第一个输入为 t1 类型,第二个输入为 t2 类型,并返回输出类型为 u 。对于采用 n 参数的函数也是如此。

Dynamic semantics. There is no dynamic semantics of function definitions. There is nothing to be evaluated. OCaml just records that the name f is bound to a function with the given arguments x1..xn and the given body e. Only later, when the function is applied, will there be some evaluation to do.
动态语义。函数定义没有动态语义。没有什么可以评价的。 OCaml 仅记录名称 f 绑定到具有给定参数 x1..xn 和给定主体 e 的函数。只有稍后应用该功能时,才会进行一些评估。

Static semantics. The static semantics of function definitions:
静态语义。函数定义的静态语义:

  • For non-recursive functions: if by assuming that x1 : t1 and x2 : t2 and … and xn : tn, we can conclude that e : u, then f : t1 -> t2 -> ... -> tn -> u.
    对于非递归函数:如果通过假设 x1 : t1x2 : t2 以及 ... 和 xn : tn ,我们可以得出 e : u 的结论,那么 f : t1 -> t2 -> ... -> tn -> u

  • For recursive functions: if by assuming that x1 : t1 and x2 : t2 and … and xn : tn and f : t1 -> t2 -> ... -> tn -> u, we can conclude that e : u, then f : t1 -> t2 -> ... -> tn -> u.
    对于递归函数:如果通过假设 x1 : t1x2 : t2 和 ... 以及 xn : tnf : t1 -> t2 -> ... -> tn -> u ,我们可以得出 e : u

Note how the type checking rule for recursive functions assumes that the function identifier f has a particular type, then checks to see whether the body of the function is well-typed under that assumption. This is because f is in scope inside the function body itself (just like the arguments are in scope).
请注意递归函数的类型检查规则如何假设函数标识符 f 具有特定类型,然后检查函数体在该假设下是否类型正确。这是因为 f 位于函数体本身的范围内(就像参数位于范围内一样)。

2.4.2. Anonymous Functions
2.4.2. 匿名函数 ¶

We already know that we can have values that are not bound to names. The integer 42, for example, can be entered at the toplevel without giving it a name:
我们已经知道我们可以拥有不与名称绑定的值。例如,可以在顶层输入整数 42 而无需为其指定名称:

42
- : int = 42

Or we can bind it to a name:
或者我们可以将它绑定到一个名称:

let x = 42
val x : int = 42

Similarly, OCaml functions do not have to have names; they may be anonymous. For example, here is an anonymous function that increments its input: fun x -> x + 1. Here, fun is a keyword indicating an anonymous function, x is the argument, and -> separates the argument from the body.
同样,OCaml 函数不必有名称;他们可能是匿名的。例如,下面是一个增加其输入的匿名函数: fun x -> x + 1 。这里, fun 是表示匿名函数的关键字, x 是参数, -> 将参数与函数体分开。

We now have two ways we could write an increment function:
现在我们有两种编写增量函数的方法:

let inc x = x + 1
let inc = fun x -> x + 1
val inc : int -> int = <fun>
val inc : int -> int = <fun>

They are syntactically different but semantically equivalent. That is, even though they involve different keywords and put some identifiers in different places, they mean the same thing.
它们在语法上不同,但在语义上等效。也就是说,即使它们涉及不同的关键字并将一些标识符放在不同的位置,但它们的含义相同。

Anonymous functions are also called lambda expressions, a term that comes from the lambda calculus, which is a mathematical model of computation in the same sense that Turing machines are a model of computation. In the lambda calculus, fun x -> e would be written λx.e. The λ denotes an anonymous function.
匿名函数也称为 lambda 表达式,该术语来自 lambda 演算,它是一种计算的数学模型,与图灵机是一种计算模型的意义相同。在 lambda 演算中, fun x -> e 将写作 λx.eλ 表示匿名函数。

It might seem a little mysterious right now why we would want functions that have no names. Don’t worry; we’ll see good uses for them later in the course, especially when we study so-called “higher-order programming”. In particular, we will often create anonymous functions and pass them as input to other functions.
现在似乎有点神秘,为什么我们需要没有名称的函数。不用担心;我们将在课程后面看到它们的良好用途,特别是当我们学习所谓的“高阶编程”时。特别是,我们经常创建匿名函数并将它们作为输入传递给其他函数。

Syntax.

fun x1 ... xn -> e

Static semantics. 静态语义。

  • If by assuming that x1 : t1 and x2 : t2 and … and xn : tn, we can conclude that e : u, then fun x1 ... xn -> e : t1 -> t2 -> ... -> tn -> u.
    如果通过假设 x1 : t1x2 : t2 和 ... 和 xn : tn ,我们可以得出 e : u ,然后 fun x1 ... xn -> e : t1 -> t2 -> ... -> tn -> u

Dynamic semantics. An anonymous function is already a value. There is no computation to be performed.
动态语义。匿名函数已经是一个值。无需执行任何计算。

2.4.3. Function Application
2.4.3. 函数应用 ¶

Here we cover a somewhat simplified syntax of function application compared to what OCaml actually allows.
与 OCaml 实际允许的语法相比,这里我们介绍了稍微简化的函数应用语法。

Syntax.

e0 e1 e2 ... en

The first expression e0 is the function, and it is applied to arguments e1 through en. Note that parentheses are not required around the arguments to indicate function application, as they are in languages in the C family, including Java.
第一个表达式 e0 是函数,它应用于参数 e1en 。请注意,参数周围不需要括号来指示函数应用程序,因为它们在 C 系列语言(包括 Java)中是这样的。

Static semantics. 静态语义。

  • If e0 : t1 -> ... -> tn -> u and e1 : t1 and … and en : tn then e0 e1 ... en : u.
    e0 : t1 -> ... -> tn -> u 以及 e1 : t1 以及 ... 以及 en : tne0 e1 ... en : u

Dynamic semantics. 动态语义。

To evaluate e0 e1 ... en: 评估 e0 e1 ... en

  1. Evaluate e0 to a function. Also evaluate the argument expressions e1 through en to values v1 through vn.
    e0 算作函数。还将参数表达式 e1en 算作值 v1vn

    For e0, the result might be an anonymous function fun x1 ... xn -> e or a name f. In the latter case, we need to find the definition of f, which we can assume to be of the form let rec f x1 ... xn = e. Either way, we now know the argument names x1 through xn and the body e.
    对于 e0 ,结果可能是匿名函数 fun x1 ... xn -> e 或名称 f 。在后一种情况下,我们需要找到 f 的定义,我们可以假设其形式为 let rec f x1 ... xn = e 。不管怎样,我们现在知道参数名称 x1xn 和正文 e

  2. Substitute each value vi for the corresponding argument name xi in the body e of the function. That substitution results in a new expression e'.
    将每个值 vi 替换为函数主体 e 中相应的参数名称 xi 。该替换会产生一个新的表达式 e'

  3. Evaluate e' to a value v, which is the result of evaluating e0 e1 ... en.
    e' 计算为值 v ,这是计算 e0 e1 ... en 的结果。

If you compare these evaluation rules to the rules for let expressions, you will notice they both involve substitution. This is not an accident. In fact, anywhere let x = e1 in e2 appears in a program, we could replace it with (fun x -> e2) e1. They are syntactically different but semantically equivalent. In essence, let expressions are just syntactic sugar for anonymous function application.
如果将这些求值规则与 let 表达式的规则进行比较,您会发现它们都涉及替换。这不是意外。事实上,程序中出现 let x = e1 in e2 的任何地方,我们都可以将其替换为 (fun x -> e2) e1 。它们在语法上不同,但在语义上等效。本质上, let 表达式只是匿名函数应用的语法糖。

2.4.4. Pipeline 2.4.4. 管道 ¶

There is a built-in infix operator in OCaml for function application called the pipeline operator, written |>. Imagine that as depicting a triangle pointing to the right. The metaphor is that values are sent through the pipeline from left to right. For example, suppose we have the increment function inc from above as well as a function square that squares its input:
OCaml 中有一个内置的用于函数应用的中缀运算符,称为管道运算符,写为 |> 。想象一下,描绘一个指向右侧的三角形。比喻是值通过管道从左到右发送。例如,假设我们有上面的增量函数 inc 以及对其输入求平方的函数 square

let square x = x * x
val square : int -> int = <fun>

Here are two equivalent ways of squaring 6:
以下是对 6 进行平方的两种等效方法:

square (inc 5);;
5 |> inc |> square;;
- : int = 36
- : int = 36

The latter uses the pipeline operator to send 5 through the inc function, then send the result of that through the square function. This is a nice, idiomatic way of expressing the computation in OCaml. The former way is arguably not as elegant: it involves writing extra parentheses and requires the reader’s eyes to jump around, rather than move linearly from left to right. The latter way scales up nicely when the number of functions being applied grows, where as the former way requires more and more parentheses:
后者使用管道运算符通过 inc 函数发送 5 ,然后通过 square 函数发送结果。这是在 OCaml 中表达计算的一种很好的、​​惯用的方式。前一种方式可以说没有那么优雅:它需要编写额外的括号,并且需要读者的眼睛来回跳动,而不是从左到右线性移动。当应用的函数数量增加时,后一种方法可以很好地扩展,而前一种方法需要越来越多的括号:

5 |> inc |> square |> inc |> inc |> square;;
square (inc (inc (square (inc 5))));;
- : int = 1444
- : int = 1444

It might feel weird at first, but try using the pipeline operator in your own code the next time you find yourself writing a big chain of function applications.
一开始可能会感觉很奇怪,但是下次当您发现自己正在编写一大串函数应用程序时,请尝试在自己的代码中使用管道运算符。

Since e1 |> e2 is just another way of writing e2 e1, we don’t need to state the semantics for |>: it’s just the same as function application. These two programs are another example of expressions that are syntactically different but semantically equivalent.
由于 e1 |> e2 只是 e2 e1 的另一种编写方式,因此我们不需要声明 |> 的语义:它与函数应用程序相同。这两个程序是语法上不同但语义上等效的表达式的另一个示例。

2.4.5. Polymorphic Functions
2.4.5. 多态函数 ¶

The identity function is the function that simply returns its input:
恒等函数是简单返回其输入的函数:

let id x = x
val id : 'a -> 'a = <fun>

Or equivalently as an anonymous function:
或者等效地作为匿名函数:

let id = fun x -> x
val id : 'a -> 'a = <fun>

The 'a is a type variable: it stands for an unknown type, just like a regular variable stands for an unknown value. Type variables always begin with a single quote. Commonly used type variables include 'a, 'b, and 'c, which OCaml programmers typically pronounce in Greek: alpha, beta, and gamma.
'a 是一个类型变量:它代表未知类型,就像常规变量代表未知值一样。类型变量始终以单引号开头。常用的类型变量包括 'a'b'c ,OCaml 程序员通常用希腊语发音:alpha、beta 和 gamma。

We can apply the identity function to any type of value we like:
我们可以将恒等函数应用于我们喜欢的任何类型的值:

id 42;;
id true;;
id "bigred";;
- : int = 42
- : bool = true
- : string = "bigred"

Because you can apply id to many types of values, it is a polymorphic function: it can be applied to many (poly) forms (morph).
因为您可以将 id 应用于多种类型的值,所以它是一个多态函数:它可以应用于多种(多)形式(morph)。

With manual type annotations, it’s possible to give a more restrictive type to a polymorphic function than the type the compiler automatically infers. For example:
通过手动类型注释,可以为多态函数提供比编译器自动推断的类型更具限制性的类型。例如:

let id_int (x : int) : int = x
val id_int : int -> int = <fun>

That’s the same function as id, except for the two manual type annotations. Because of those, we cannot apply id_int to a bool like we did id:
除了两个手动类型注释之外,它与 id 具有相同的功能。因此,我们不能像 id 那样将 id_int 应用于 bool :

id_int true
File "[14]", line 1, characters 7-11:
1 | id_int true
           ^^^^
Error: This expression has type bool but an expression was expected of type
         int

Another way of writing id_int would be in terms of id:
id_int 的另一种书写方式是 id

let id_int' : int -> int = id
val id_int' : int -> int = <fun>

In effect we took a value of type 'a -> 'a, and we bound it to a name whose type was manually specified as being int -> int. You might ask, why does that work? They aren’t the same types, after all.
实际上,我们采用了 'a -> 'a 类型的值,并将其绑定到手动指定类型为 int -> int 的名称。你可能会问,为什么这样有效?毕竟,它们不是同一类型。

One way to think about this is in terms of behavior. The type of id_int specifies one aspect of its behavior: given an int as input, it promises to produce an int as output. It turns out that id also makes the same promise: given an int as input, it too will return an int as output. Now id also makes many more promises, such as: given a bool as input, it will return a bool as output. So by binding id to a more restrictive type int -> int, we have thrown away all those additional promises as irrelevant. Sure, that’s information lost, but at least no promises will be broken. It’s always going to be safe to use a function of type 'a -> 'a when what we needed was a function of type int -> int.
思考这个问题的一种方法是从行为角度。 id_int 的类型指定了其行为的一方面:给定 int 作为输入,它承诺生成 int 作为输出。事实证明 id 也做出了相同的承诺:给定 int 作为输入,它也将返回 int 作为输出。现在 id 还做出了更多承诺,例如:给定 bool 作为输入,它将返回 bool 作为输出。因此,通过将 id 绑定到更具限制性的类型 int -> int ,我们丢弃了所有这些无关的附加承诺。当然,这会丢失信息,但至少不会违背任何承诺。当我们需要的是 int -> int 类型的函数时,使用 'a -> 'a 类型的函数总是安全的。

The converse is not true. If we needed a function of type 'a -> 'a but tried to use a function of type int -> int, we’d be in trouble as soon as someone passed an input of another type, such as bool. To prevent that trouble, OCaml does something potentially surprising with the following code:
反之则不然。如果我们需要 'a -> 'a 类型的函数,但尝试使用 int -> int 类型的函数,那么一旦有人传递另一种类型的输入,例如 bool 。为了防止这种麻烦,OCaml 使用以下代码做了一些可能令人惊讶的事情:

let id' : 'a -> 'a = fun x -> x + 1
val id' : int -> int = <fun>

Function id' is actually the increment function, not the identity function. So passing it a bool or string or some complicated data structure is not safe; the only data + can safely manipulate are integers. OCaml therefore instantiates the type variable 'a to int, thus preventing us from applying id' to non-integers:
函数 id' 实际上是增量函数,而不是恒等函数。因此向其传递 boolstring 或一些复杂的数据结构是不安全的; + 唯一可以安全操作的数据是整数。因此,OCaml 将类型变量 'a 实例化为 int ,从而阻止我们将 id' 应用于非整数:

id' true
File "[17]", line 1, characters 4-8:
1 | id' true
        ^^^^
Error: This expression has type bool but an expression was expected of type
         int

That leads us to another, more mechanical, way to think about all of this in terms of application. By that we mean the very same notion of how a function is applied to arguments: when evaluating the application id 5, the argument x is instantiated with value 5. Likewise, the 'a in the type of id is being instantiated with type int at that application. So if we write
这引导我们采用另一种更机械的方式来从应用角度思考所有这些。我们指的是函数如何应用于参数的相同概念:在评估应用程序 id 5 时,参数 x 用值 5 实例化。同样, id 类型中的 'a 在该应用程序中使用 int 类型进行实例化。所以如果我们写

let id_int' : int -> int = id
val id_int' : int -> int = <fun>

we are in fact instantiating the 'a in the type of id with the type int. And just as there is no way to “unapply” a function—for example, given 5 we can’t compute backwards to id 5—we can’t unapply that type instantiation and change int back to 'a.
事实上,我们正在使用 int 类型实例化 id 类型中的 'a 。正如没有办法“取消应用”函数一样——例如,给定 5 我们无法向后计算到 id 5 ——我们无法取消应用该类型实例化并进行更改 int 回到 'a

To make that precise, suppose we have a let definition [or expression]:
为了准确起见,假设我们有一个 let 定义[或表达式]:

let x = e [in e']

and that OCaml infers x has a type t that includes some type variables 'a, 'b, etc. Then we are permitted to instantiate those type variables. We can do that by applying the function to arguments that reveal what the type instantiations should be (as in id 5) or by a type annotation (as in id_int'), among other ways. But we have to be consistent with the instantiation. For example, we cannot take a function of type 'a -> 'b -> 'a and instantiate it at type int -> 'b -> string, because the instantiation of 'a is not the same type in each of the two places it occurred:
并且 OCaml 推断 x 具有类型 t ,其中包括一些类型变量 'a'b 等。然后我们可以实例化那些类型变量。我们可以通过将函数应用于揭示类型实例化应该是什么的参数(如 id 5 )或通过类型注释(如 id_int' )等方式来做到这一点。但我们必须与实例化保持一致。例如,我们不能采用 'a -> 'b -> 'a 类型的函数并在 int -> 'b -> string 类型上实例化它,因为 'a 的实例化在每个函数中都不是相同的类型。发生的地方有两个:

let first x y = x;;
let first_int : int -> 'b -> int = first;;
let bad_first : int -> 'b -> string = first;;
val first : 'a -> 'b -> 'a = <fun>
val first_int : int -> 'b -> int = <fun>
File "[19]", line 3, characters 38-43:
3 | let bad_first : int -> 'b -> string = first;;
                                          ^^^^^
Error: This expression has type int -> 'b -> int
       but an expression was expected of type int -> 'b -> string
       Type int is not compatible with type string 

2.4.6. Labeled and Optional Arguments
2.4.6. 带标签和可选参数 ¶

The type and name of a function usually give you a pretty good idea of what the arguments should be. However, for functions with many arguments (especially arguments of the same type), it can be useful to label them. For example, you might guess that the function String.sub returns a substring of the given string (and you would be correct). You could type in String.sub to find its type:
函数的类型和名称通常可以让您很好地了解参数应该是什么。但是,对于具有多个参数(尤其是相同类型的参数)的函数,对它们进行标记可能很有用。例如,您可能猜测函数 String.sub 返回给定字符串的子字符串(您是正确的)。您可以输入 String.sub 来查找其类型:

String.sub;;
- : string -> int -> int -> string = <fun>

But it’s not clear from the type how to use it—you’re forced to consult the documentation.
但从类型上并不清楚如何使用它——你不得不查阅文档。

OCaml supports labeled arguments to functions. You can declare this kind of function using the following syntax:
OCaml 支持函数的标记参数。您可以使用以下语法声明此类函数:

let f ~name1:arg1 ~name2:arg2 = arg1 + arg2;;
val f : name1:int -> name2:int -> int = <fun>

This function can be called by passing the labeled arguments in either order:
可以通过按任一顺序传递带标签的参数来调用此函数:

f ~name2:3 ~name1:4

Labels for arguments are often the same as the variable names for them. OCaml provides a shorthand for this case. The following are equivalent:
参数的标签通常与其变量名称相同。 OCaml 提供了这种情况的简写。以下是等效的:

let f ~name1:name1 ~name2:name2 = name1 + name2
let f ~name1 ~name2 = name1 + name2

Use of labeled arguments is largely a matter of taste. They convey extra information, but they can also add clutter to types.
使用带标签的参数很大程度上取决于品味。它们传达额外的信息,但也会给类型带来混乱。

The syntax to write both a labeled argument and an explicit type annotation for it is:
为其编写带标签参数和显式类型注释的语法是:

let f ~name1:(arg1 : int) ~name2:(arg2 : int) = arg1 + arg2

It is also possible to make some arguments optional. When called without an optional argument, a default value will be provided. To declare such a function, use the following syntax:
也可以使一些参数可选。当不带可选参数调用时,将提供默认值。要声明这样的函数,请使用以下语法:

let f ?name:(arg1=8) arg2 = arg1 + arg2
val f : ?name:int -> int -> int = <fun>

You can then call a function with or without the argument:
然后您可以调用带或不带参数的函数:

f ~name:2 7
- : int = 9
f 7
- : int = 15

2.4.7. Partial Application
2.4.7. 部分应用 ¶

We could define an addition function as follows:
我们可以定义一个加法函数如下:

let add x y = x + y
val add : int -> int -> int = <fun>

Here’s a rather similar function:
这是一个相当相似的函数:

let addx x = fun y -> x + y
val addx : int -> int -> int = <fun>

Function addx takes an integer x as input and returns a function of type int -> int that will add x to whatever is passed to it.
函数 addx 将整数 x 作为输入,并返回一个 int -> int 类型的函数,该函数会将 x 添加到传递给它的任何内容中。

The type of addx is int -> int -> int. The type of add is also int -> int -> int. So from the perspective of their types, they are the same function. But the form of addx suggests something interesting: we can apply it to just a single argument.
addx 的类型是 int -> int -> intadd 的类型也是 int -> int -> int 。所以从它们的类型来看,它们的功能是相同的。但是 addx 的形式暗示了一些有趣的事情:我们可以将它应用于单个参数。

let add5 = addx 5
val add5 : int -> int = <fun>
add5 2
- : int = 7

It turns out the same can be done with add:
事实证明 add 也可以做到同样的事情:

let add5 = add 5
val add5 : int -> int = <fun>
add5 2;;
- : int = 7

What we just did is called partial application: we partially applied the function add to one argument, even though you would normally think of it as a multi-argument function. This works because the following three functions are syntactically different but semantically equivalent. That is, they are different ways of expressing the same computation:
我们刚刚所做的称为部分应用:我们将函数 add 部分应用到一个参数,即使您通常将其视为多参数函数。这是可行的,因为以下三个函数在语法上不同但在语义上等效。也就是说,它们是表达相同计算的不同方式:

let add x y = x + y
let add x = fun y -> x + y
let add = fun x -> (fun y -> x + y)

So add is really a function that takes an argument x and returns a function (fun y -> x + y). Which leads us to a deep truth…
所以 add 实际上是一个接受参数 x 并返回函数 (fun y -> x + y) 的函数。这让我们发现了一个深刻的真理……

2.4.8. Function Associativity
2.4.8. 函数结合率 ¶

Are you ready for the truth? Take a deep breath. Here goes…
你准备好接受真相了吗?深吸一口气。开始…

Every OCaml function takes exactly one argument.
每个 OCaml 函数只接受一个参数。

Why? Consider add: although we can write it as let add x y = x + y, we know that’s semantically equivalent to let add = fun x -> (fun y -> x + y). And in general,
为什么?考虑 add :虽然我们可以将其写为 let add x y = x + y ,但我们知道它在语义上等同于 let add = fun x -> (fun y -> x + y) 。一般来说,

let f x1 x2 ... xn = e

is semantically equivalent to
在语义上等价于

let f =
  fun x1 ->
    (fun x2 ->
       (...
          (fun xn -> e)...))

So even though you think of f as a function that takes n arguments, in reality it is a function that takes 1 argument and returns a function.
因此,即使您将 f 视为采用 n 参数的函数,但实际上它是一个采用 1 个参数并返回一个函数的函数。

The type of such a function
这种函数的类型

t1 -> t2 -> t3 -> t4

really means the same as
真正的意思是一样的

t1 -> (t2 -> (t3 -> t4))

That is, function types are right associative: there are implicit parentheses around function types, from right to left. The intuition here is that a function takes a single argument and returns a new function that expects the remaining arguments.
也就是说,函数类型是右关联的:函数类型周围有隐式括号,从右到左。这里的直觉是一个函数接受一个参数并返回一个需要剩余参数的新函数。

Function application, on the other hand, is left associative: there are implicit parenthesis around function applications, from left to right. So
另一方面,函数应用程序是左关联的:从左到右,函数应用程序周围有隐式括号。所以

e1 e2 e3 e4

really means the same as
真正的意思是一样的

((e1 e2) e3) e4

The intuition here is that the left-most expression grabs the next expression to its right as its single argument.
这里的直觉是,最左边的表达式抓取其右侧的下一个表达式作为其单个参数。

2.4.9. Operators as Functions
2.4.9. 作为函数的运算符 ¶

The addition operator + has type int -> int -> int. It is normally written infix, e.g., 3 + 4. By putting parentheses around it, we can make it a prefix operator:
加法运算符 + 的类型为 int -> int -> int 。它通常写成中缀,例如 3 + 4 。通过在它周围放置括号,我们可以将其设为前缀运算符:

( + )
- : int -> int -> int = <fun>
( + ) 3 4;;
- : int = 7
let add3 = ( + ) 3
val add3 : int -> int = <fun>
add3 2
- : int = 5

The same technique works for any built-in operator.
相同的技术适用于任何内置运算符。

Normally the spaces are unnecessary. We could write (+) or ( + ), but it is best to include them. Beware of multiplication, which must be written as ( * ), because (*) would be parsed as beginning a comment.
通常这些空格是不必要的。我们可以编写 (+)( + ) ,但最好包含它们。请注意乘法,它必须写为 ( * ) ,因为 (*) 会被解析为注释的开始。

We can even define our own new infix operators, for example:
我们甚至可以定义自己的新中缀运算符,例如:

let ( ^^ ) x y = max x y

And now 2 ^^ 3 evaluates to 3.
现在 2 ^^ 3 的计算结果为 3

The rules for which punctuation can be used to create infix operators are not necessarily intuitive. Nor is the relative precedence with which such operators will be parsed. So be careful with this usage.
使用标点符号创建中缀运算符的规则不一定直观。解析此类运算符的相对优先级也不是。所以要小心这种用法。

2.4.10. Tail Recursion 2.4.10. 尾递归 ¶

Consider the following seemingly uninteresting function, which counts from 1 to n:
考虑以下看似无趣的函数,它从 1 计数到 n

(** [count n] is [n], computed by adding 1 to itself [n] times.  That is,
    this function counts up from 1 to [n]. *)
let rec count n =
  if n = 0 then 0 else 1 + count (n - 1)
val count : int -> int = <fun>

Counting to 10 is no problem:
数到 10 没有问题:

count 10
- : int = 10

Counting to 100,000 is no problem either:
数到 100,000 也没有问题:

count 100_000
- : int = 100000

But try counting to 1,000,000 and you’ll get the following error:
但尝试数到 1,000,000 时,您会得到以下错误:

Stack overflow during evaluation (looping recursion?).

What’s going on here?
这里发生了什么?

The Call Stack. The issue is that the call stack has a limited size. You probably learned in one of your introductory programming classes that most languages implement function calls with a stack. That stack contains one element for each function call that has been started but has not yet completed. Each element stores information like the values of local variables and which instruction in the function is currently being executed. When the evaluation of one function body calls another function, a new element is pushed on the call stack, and it is popped off when the called function completes.
调用堆栈。问题是调用堆栈的大小有限。您可能在一门入门编程课程中了解到,大多数语言都使用堆栈实现函数调用。该堆栈包含每个已启动但尚未完成的函数调用的一个元素。每个元素存储诸如局部变量的值以及当前正在执行函数中的哪条指令等信息。当一个函数体的求值调用另一个函数时,一个新元素会被压入调用堆栈,并在被调用函数完成时弹出。

The size of the stack is usually limited by the operating system. So if the stack runs out of space, it becomes impossible to make another function call. Normally this doesn’t happen, because there’s no reason to make that many successive function calls before returning. In cases where it does happen, there’s good reason for the operating system to make that program stop: it might be in the process of eating up all the memory available on the entire computer, thus harming other programs running on the same computer. The count function isn’t likely to do that, but this function would:
堆栈的大小通常受到操作系统的限制。因此,如果堆栈空间不足,就不可能进行另一个函数调用。通常这种情况不会发生,因为没有理由在返回之前进行多次连续的函数调用。如果确实发生这种情况,操作系统有充分的理由让该程序停止:它可能正在耗尽整个计算机上的所有可用内存,从而损害同一台计算机上运行的其他程序。 count 函数不太可能执行此操作,但此函数会:

let rec count_forever n = 1 + count_forever n
val count_forever : 'a -> int = <fun>

So the operating system for safety’s sake limits the call stack size. That means eventually count will run out of stack space on a large enough input. Notice how that choice is really independent of the programming language. So this same issue can and does occur in languages other than OCaml, including Python and Java. You’re just less likely to have seen it manifest there, because you probably never wrote quite as many recursive functions in those languages.
所以操作系统为了安全起见限制了调用栈的大小。这意味着最终 count 将在足够大的输入上耗尽堆栈空间。请注意该选择如何真正独立于编程语言。因此,同样的问题可能也确实发生在 OCaml 以外的语言中,包括 Python 和 Java。您只是不太可能在那里看到它,因为您可能从未用这些语言编写过那么多的递归函数。

Tail Recursion. There is a solution to this issue that was described in a 1977 paper about LISP by Guy Steele. The solution, tail-call optimization, requires some cooperation between the programmer and the compiler. The programmer does a little rewriting of the function, which the compiler then notices and applies an optimization. Let’s see how it works.
尾递归。 Guy Steele 在 1977 年关于 LISP 的论文中描述了这个问题的解决方案。尾部调用优化的解决方案需要程序员和编译器之间的一些合作。程序员对函数进行了一些重写,然后编译器会注意到并应用优化。让我们看看它是如何工作的。

Suppose that a recursive function f calls itself then returns the result of that recursive call. Our count function does not do that:
假设递归函数 f 调用自身,然后返回该递归调用的结果。我们的 count 函数不会这样做:

let rec count n =
  if n = 0 then 0 else 1 + count (n - 1)
val count : int -> int = <fun>

Rather, after the recursive call count (n - 1), there is computation remaining: the computer still needs to add 1 to the result of that call.
相反,在递归调用 count (n - 1) 之后,还剩下计算:计算机仍然需要将 1 添加到该调用的结果中。

But we as programmers could rewrite the count function so that it does not need to do any additional computation after the recursive call. The trick is to create a helper function with an extra parameter:
但是我们作为程序员可以重写 count 函数,使其在递归调用后不需要进行任何额外的计算。诀窍是创建一个带有额外参数的辅助函数:

let rec count_aux n acc =
  if n = 0 then acc else count_aux (n - 1) (acc + 1)

let count_tr n = count_aux n 0
val count_aux : int -> int -> int = <fun>
val count_tr : int -> int = <fun>

Function count_aux is almost the same as our original count, but it adds an extra parameter named acc, which is idiomatic and stands for “accumulator”. The idea is that the value we want to return from the function is slowly, with each recursive call, being accumulated in it. The “remaining computation” —the addition of 1— now happens before the recursive call not after. When the base case of the recursion finally arrives, the function now returns acc, where the answer has been accumulated.
函数 count_aux 与我们原来的 count 几乎相同,但它添加了一个名为 acc 的额外参数,这是惯用的,代表“累加器”。这个想法是,我们想要从函数返回的值会随着每次递归调用而慢慢地累积在其中。 “剩余计算”——加 1——现在发生在递归调用之前而不是之后。当递归的基本情况最终到达时,函数现在返回 acc ,其中已累积答案。

But the original base case of 0 still needs to exist in the code somewhere. And it does, as the original value of acc that is passed to count_aux. Now count_tr (we’ll get to why the name is “tr” in just a minute) works as a replacement for our original count.
但原始的基本情况 0 仍然需要存在于代码中的某个地方。确实如此,作为传递给 count_auxacc 原始值。现在 count_tr (我们很快就会明白为什么名称是“tr”)可以替代我们原来的 count

At this point we’ve completed the programmer’s responsibility, but it’s probably not clear why we went through this effort. After all count_aux will still call itself recursively too many times as count did, and eventually overflow the stack.
至此,我们已经完成了程序员的职责,但可能还不清楚我们为什么要付出这样的努力。毕竟 count_aux 仍然会像 count 一样递归调用自身太多次,并最终溢出堆栈。

That’s where the compiler’s responsibility kicks in. A good compiler (and the OCaml compiler is good this way) can notice when a recursive call is in tail position, which is a technical way of saying “there’s no more computation to be done after it returns”. The recursive call to count_aux is in tail position; the recursive call to count is not. Here they are again so you can compare them:
这就是编译器的责任发挥作用的地方。一个好的编译器(OCaml 编译器在这方面就很好)可以注意到递归调用何时处于尾部位置,这是一种技术方式,表示“返回后不再需要进行任何计算” ”。对 count_aux 的递归调用位于尾部位置;对 count 的递归调用不是。它们再次出现,以便您可以比较它们:

let rec count n =
  if n = 0 then 0 else 1 + count (n - 1)

let rec count_aux n acc =
  if n = 0 then acc else count_aux (n - 1) (acc + 1)

Here’s why tail position matters: A recursive call in tail position does not need a new stack frame. It can just reuse the existing stack frame. That’s because there’s nothing left of use in the existing stack frame! There’s no computation left to be done, so none of the local variables, or next instruction to execute, etc. matter any more. None of that memory ever needs to be read again, because that call is effectively already finished. So instead of wasting space by allocating another stack frame, the compiler “recycles” the space used by the previous frame.
这就是尾部位置很重要的原因:尾部位置的递归调用不需要新的堆栈帧。它可以只重用现有的堆栈框架。那是因为现有的堆栈框架中没有任何东西可以使用了!没有任何计算需要完成,因此局部变量或要执行的下一条指令等都不再重要。这些内存都不需要再次读取,因为该调用实际上已经完成。因此,编译器不会通过分配另一个堆栈帧来浪费空间,而是“回收”前一帧使用的空间。

This is the tail-call optimization. It can even be applied in cases beyond recursive functions if the calling function’s stack frame is suitably compatible with the callee. And, it’s a big deal. The tail-call optimization reduces the stack space requirements from linear to constant. Whereas count needed O(n) stack frames, count_aux needs only O(1), because the same frame gets reused over and over again for each recursive call. And that means count_tr actually can count to 1,000,000:
这就是尾调用优化。如果调用函数的堆栈帧与被调用者适当兼容,它甚至可以应用于递归函数之外的情况。而且,这是一件大事。尾部调用优化将堆栈空间需求从线性降低到恒定。 count 需要 O(n) 堆栈帧,而 count_aux 仅需要 O(1) ,因为每次递归调用都会一遍又一遍地重复使用同一帧。这意味着 count_tr 实际上可以数到 1,000,000:

count_tr 1_000_000
- : int = 1000000

Finally, why did we name this function count_tr? The “tr” stands for tail recursive. A tail recursive function is a recursive function whose recursive calls are all in tail position. In other words, it’s a function that (unless there are other pathologies) will not exhaust the stack.
最后,为什么我们将这个函数命名为 count_tr ? “tr”代表尾递归。尾递归函数是递归调用全部位于尾部位置的递归函数。换句话说,这个函数(除非有其他问题)不会耗尽堆栈。

The Importance of Tail Recursion. Sometimes beginning functional programmers fixate a bit too much upon it. If all you care about is writing the first draft of a function, you probably don’t need to worry about tail recursion. It’s pretty easy to make it tail recursive later if you need to, just by adding an accumulator argument. Or maybe you should rethink how you have designed the function. Take count, for example: it’s kind of dumb. But later we’ll see examples that aren’t dumb, such as iterating over lists with thousands of elements.
尾递归的重要性。有时,刚开始的函数式程序员对此过于关注。如果您只关心编写函数的初稿,那么您可能不需要担心尾递归。如果需要的话,稍后可以很容易地使其尾递归,只需添加一个累加器参数即可。或者也许您应该重新考虑如何设计该功能。以 count 为例:这有点愚蠢。但稍后我们会看到一些并不愚蠢的示例,例如迭代具有数千个元素的列表。

It is important that the compiler support the optimization. Otherwise, the transformation you do to the code as a programmer makes no difference. Indeed, most compilers do support it, at least as an option. Java is a notable exception.
编译器支持优化非常重要。否则,作为程序员对代码所做的转换没有任何区别。事实上,大多数编译器都支持它,至少作为一个选项。 Java 是一个值得注意的例外。

The Recipe for Tail Recursion. In a nutshell, here’s how we made a function be tail recursive:
尾递归的秘诀。简而言之,我们是如何使函数成为尾递归的:

  1. Change the function into a helper function. Add an extra argument: the accumulator, often named acc.
    将函数更改为辅助函数。添加一个额外的参数:累加器,通常命名为 acc

  2. Write a new “main” version of the function that calls the helper. It passes the original base case’s return value as the initial value of the accumulator.
    编写调用帮助器的函数的新“主”版本。它将原始基本情况的返回值作为累加器的初始值传递。

  3. Change the helper function to return the accumulator in the base case.
    更改辅助函数以返回基本情况下的累加器。

  4. Change the helper function’s recursive case. It now needs to do the extra work on the accumulator argument, before the recursive call. This is the only step that requires much ingenuity.
    更改辅助函数的递归大小写。现在,它需要在递归调用之前对累加器参数执行额外的工作。这是唯一需要很多独创性的步骤。

An Example: Factorial. Let’s transform this factorial function to be tail recursive:
一个例子:阶乘。让我们将这个阶乘函数转换为尾递归:

(* [fact n] is [n] factorial *)
let rec fact n =
  if n = 0 then 1 else n * fact (n - 1)
val fact : int -> int = <fun>

First, we change its name and add an accumulator argument:
首先,我们更改它的名称并添加一个累加器参数:

let rec fact_aux n acc = ...

Second, we write a new “main” function that calls the helper with the original base case as the accumulator:
其次,我们编写一个新的“main”函数,该函数使用原始基本情况作为累加器来调用助手:

let rec fact_tr n = fact_aux n 1

Third, we change the helper function to return the accumulator in the base case:
第三,我们更改辅助函数以在基本情况下返回累加器:

if n = 0 then acc ...

Finally, we change the recursive case:
最后,我们改变递归的情况:

else fact_aux (n - 1) (n * acc)

Putting it all together, we have:
把它们放在一起,我们有:

let rec fact_aux n acc =
  if n = 0 then acc else fact_aux (n - 1) (n * acc)

let fact_tr n = fact_aux n 1
val fact_aux : int -> int -> int = <fun>
val fact_tr : int -> int = <fun>

It was a nice exercise, but maybe not worthwhile. Even before we exhaust the stack space, the computation suffers from integer overflow:
这是一个很好的练习,但可能不值得。甚至在我们耗尽堆栈空间之前,计算就会遇到整数溢出:

fact 50
- : int = -3258495067890909184

To solve that problem, we turn to OCaml’s big integer library, Zarith. Here we use a few OCaml features that are beyond anything we’ve seen so far, but hopefully nothing terribly surprising. (If you want to follow along with this code, first install Zarith in OPAM with opam install zarith.)
为了解决这个问题,我们求助于 OCaml 的大整数库 Zarith。在这里,我们使用了一些 OCaml 功能,这些功能超出了我们迄今为止所见过的任何功能,但希望没有什么特别令人惊讶的。 (如果您想遵循此代码,请首先使用 opam install zarith 在 OPAM 中安装 Zarith。)

#require "zarith.top";;
let rec zfact_aux n acc =
  if Z.equal n Z.zero then acc else zfact_aux (Z.pred n) (Z.mul acc n);;

let zfact_tr n = zfact_aux n Z.one;;

zfact_tr (Z.of_int 50)
val zfact_aux : Z.t -> Z.t -> Z.t = <fun>
val zfact_tr : Z.t -> Z.t = <fun>
- : Z.t = 30414093201713378043612608166064768844377641568960512000000000000

If you want you can use that code to compute zfact_tr 1_000_000 without stack or integer overflow, though it will take several minutes.
如果您愿意,可以使用该代码来计算 zfact_tr 1_000_000 而无需堆栈或整数溢出,尽管这需要几分钟的时间。

The chapter on modules will explain the OCaml features we used above in detail, but for now:
关于模块的章节将详细解释我们上面使用的 OCaml 功能,但现在:

  • #require loads the library, which provides a module named Z. Recall that Z is the symbol used in mathematics to denote the integers.
    #require 加载库,它提供了一个名为 Z 的模块。回想一下, Z 是数学中用来表示整数的符号。

  • Z.n means the name n defined inside of Z.
    Z.n 表示 Z 内部定义的名称 n

  • The type Z.t is the library’s name for the type of big integers.
    类型 Z.t 是大整数类型的库名称。

  • We use library values Z.equal for equality comparison, Z.zero for 0, Z.pred for predecessor (i.e., subtracting 1), Z.mul for multiplication, Z.one for 1, and Z.of_int to convert a primitive integer to a big integer.
    我们使用库值 Z.equal 进行相等比较, Z.zero 表示 0, Z.pred 表示前驱(即减 1), Z.mul 表示乘法、 Z.one 表示 1, Z.of_int 将原始整数转换为大整数。

2.5. Documentation 2.5. 文档 ¶

OCaml provides a tool called OCamldoc that works a lot like Java’s Javadoc tool: it extracts specially formatted comments from source code and renders them as HTML, making it easy for programmers to read documentation.
OCaml 提供了一个名为 OCamldoc 的工具,它的工作方式很像 Java 的 Javadoc 工具:它从源代码中提取特殊格式的注释并将其呈现为 HTML,使程序员可以轻松阅读文档。

2.5.1. How to Document
2.5.1. 如何记录 ¶

Here’s an example of an OCamldoc comment:
以下是 OCamldoc 注释的示例:

(** [sum lst] is the sum of the elements of [lst]. *)
let rec sum lst = ...
  • The double asterisk is what causes the comment to be recognized as an OCamldoc comment.
    双星号导致注释被识别为 OCamldoc 注释。

  • The square brackets around parts of the comment mean that those parts should be rendered in HTML as typewriter font rather than the regular font.
    注释部分周围的方括号表示这些部分应在 HTML 中呈现为 typewriter font 而不是常规字体。

Also like Javadoc, OCamldoc supports documentation tags, such as @author, @deprecated, @param, @return, etc. For example, in the first line of most programming assignments, we ask you to complete a comment like this:
与 Javadoc 一样,OCamldoc 支持文档标记,例如 @author@deprecated@param@return 等。在大多数编程作业的第一行,我们要求您完成如下评论:

(** @author Your Name (your netid) *)

For the full range of possible markup inside a OCamldoc comment, see the OCamldoc manual. But what we’ve covered here is good enough for most documentation that you’ll need to write.
有关 OCamldoc 注释中所有可能的标记,请参阅 OCamldoc 手册。但我们在这里介绍的内容对于您需要编写的大多数文档来说已经足够了。

2.5.2. What to Document
2.5.2. 记录什么内容 ¶

The documentation style we favor in this book resembles that of the OCaml standard library: concise and declarative. As an example, let’s revisit the documentation of sum:
我们在本书中喜欢的文档风格类似于 OCaml 标准库的文档风格:简洁且声明式。作为一个例子,让我们回顾一下 sum 的文档:

(** [sum lst] is the sum of the elements of [lst]. *)
let rec sum lst = ...

That comment starts with sum lst, which is an example application of the function to an argument. The comment continues with the word “is”, thus declaratively describing the result of the application. (The word “returns” could be used instead, but “is” emphasizes the mathematical nature of the function.) That description uses the name of the argument, lst, to explain the result.
该注释以 sum lst 开头,这是该函数对参数的示例应用。该注释以“is”一词继续,从而声明性地描述了应用程序的结果。 (可以使用“returns”一词来代替,但“is”强调函数的数学本质。)该描述使用参数的名称 lst 来解释结果。

Note how there is no need to add tags to redundantly describe parameters or return values, as is often done with Javadoc. Everything that needs to be said has already been said. We strongly discourage documentation like the following:
请注意,无需添加标签来冗余地描述参数或返回值,而 Javadoc 经常这样做。该说的都已经说了。我们强烈反对如下文档:

(** Sum a list.
    @param lst The list to be summed.
    @return The sum of the list. *)
let rec sum lst = ...

That poor documentation takes three needlessly hard-to-read lines to say the same thing as the limpid one-line version.
这份糟糕的文档需要三行不必要的难以阅读的行来表达与清晰的一行版本相同的内容。

There is one way we might improve the documentation we have so far, which is to explicitly state what happens with empty lists:
有一种方法可以改进我们目前拥有的文档,即明确说明空列表会发生什么:

(** [sum lst] is the sum of the elements of [lst].
    The sum of an empty list is 0. *)
let rec sum lst = ...

2.5.3. Preconditions and Postconditions
2.5.3. 前提条件和后置条件 ¶

Here are a few more examples of comments written in the style we favor.
以下是一些以我们喜欢的风格撰写的评论示例。

(** [lowercase_ascii c] is the lowercase ASCII equivalent of
    character [c]. *)

(** [index s c] is the index of the first occurrence of
    character [c] in string [s].  Raises: [Not_found]
    if [c] does not occur in [s]. *)

(** [random_int bound] is a random integer between 0 (inclusive)
    and [bound] (exclusive).  Requires: [bound] is greater than 0
    and less than 2^30. *)

The documentation of index specifies that the function raises an exception, as well as what that exception is and the condition under which it is raised. (We will cover exceptions in more detail in the next chapter.) The documentation of random_int specifies that the function’s argument must satisfy a condition.
index 的文档指定该函数引发异常,以及该异常是什么以及引发该异常的条件。 (我们将在下一章中更详细地介绍异常。) random_int 的文档指定函数的参数必须满足条件。

In previous courses, you were exposed to the ideas of preconditions and postconditions. A precondition is something that must be true before some section of code; and a postcondition, after.
在之前的课程中,您接触过前置条件和后置条件的概念。前提条件是在代码的某些部分之前必须为真的东西;和后置条件,after。

The “Requires” clause above in the documentation of random_int is a kind of precondition. It says that the client of the random_int function is responsible for guaranteeing something about the value of bound. Likewise, the first sentence of that same documentation is a kind of postcondition. It guarantees something about the value returned by the function.
上面 random_int 文档中的“Requires”子句是一种前提条件。它表示 random_int 函数的客户端负责保证 bound 的值。同样,同一文档的第一句话是一种后置条件。它保证了函数返回的值。

The “Raises” clause in the documentation of index is another kind of postcondition. It guarantees that the function raises an exception. Note that the clause is not a precondition, even though it states a condition in terms of an input.
index 文档中的“Raises”子句是另一种后置条件。它保证该函数引发异常。请注意,该子句不是前提条件,尽管它以输入的形式陈述了条件。

Note that none of these examples has a “Requires” clause that says something about the type of an input. If you’re coming from a dynamically-typed language, like Python, this could be a surprise. Python programmers frequently document preconditions regarding the types of function inputs. OCaml programmers, however, do not. That’s because the compiler itself does the type checking to ensure that you never pass a value of the wrong type to a function. Consider lowercase_ascii again: although the English comment helpfully identifies the type of c to the reader, the comment does not state a “Requires” clause like this:
请注意,这些示例都没有“Requires”子句来说明有关输入类型的信息。如果您使用的是动态类型语言(例如 Python),这可能会让人感到惊讶。 Python 程序员经常记录有关函数输入类型的前提条件。然而,OCaml 程序员却不这么认为。这是因为编译器本身会进行类型检查,以确保您永远不会将错误类型的值传递给函数。再次考虑 lowercase_ascii :尽管英文注释有助于向读者标识 c 的类型,但该注释并未声明如下所示的“Requires”子句:

(** [lowercase_ascii c] is the lowercase ASCII equivalent of [c].
    Requires: [c] is a character. *)

Such a comment reads as highly unidiomatic to an OCaml programmer, who would read that comment and be puzzled, perhaps thinking: “Well of course c is a character; the compiler will guarantee that. What did the person who wrote that really mean? Is there something they or I am missing?”
对于 OCaml 程序员来说,这样的注释读起来非常不习惯,他们会读到该注释并感到困惑,也许会想:“当然 c 是一个字符;编译器会保证这一点。写这句话的人到底想表达什么意思?他们或我是否遗漏了什么?”

2.6. Printing 2.6. 印刷术 ¶

OCaml has built-in printing functions for a few of the built-in primitive types: print_char, print_string, print_int, and print_float. There’s also a print_endline function, which is like print_string, but also outputs a newline.
OCaml 具有针对一些内置基元类型的内置打印函数: print_charprint_stringprint_intprint_float 。还有一个 print_endline 函数,它类似于 print_string ,但也输出换行符。

print_endline "Camels are bae"
Camels are bae
- : unit = ()

2.6.1. Unit 2.6.1. 单元 ¶

Let’s look at the types of a couple of those functions:
让我们看一下其中几个函数的类型:

print_endline
- : string -> unit = <fun>
print_string
- : string -> unit = <fun>

They both take a string as input and return a value of type unit, which we haven’t seen before. There is only one value of this type, which is written () and is also pronounced “unit”. So unit is like bool, except there is one fewer value of type unit than there is of bool.
它们都接受字符串作为输入并返回 unit 类型的值,这是我们以前从未见过的。这种类型只有一个值,写作 () ,也读作“unit”。因此 unit 类似于 bool ,只不过 unit 类型的值比 bool 类型的值少一个。

Unit is used when you need to take an argument or return a value, but there’s no interesting value to pass or return. It is the equivalent of void in Java, and is similar to None in Python. Unit is often used when you’re writing or using code that has side effects. Printing is an example of a side effect: it changes the world and can’t be undone.
当您需要接受参数或返回值,但没有有趣的值可传递或返回时,可以使用 Unit。它相当于Java中的 void ,类似于Python中的 None 。当您编写或使用有副作用的代码时,通常会使用单位。打印是副作用的一个例子:它改变了世界并且无法挽回。

2.6.2. Semicolon 2.6.2. 分号 ¶

If you want to print one thing after another, you could sequence some print functions using nested let expressions:
如果你想打印一件又一件的事情,你可以使用嵌套的 let 表达式对一些打印函数进行排序:

let _ = print_endline "Camels" in
let _ = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()

The let _ = e syntax above is a way of evaluating e but not binding its value to any name. Indeed, we know the value each of those print_endline functions will return: it will always be (), the unit value. So there’s no good reason to bind it to a variable name. We could also write let () = e to indicate we know it’s just a unit value that we don’t care about:
上面的 let _ = e 语法是一种评估 e 但不将其值绑定到任何名称的方法。事实上,我们知道每个 print_endline 函数将返回的值:它始终是 () ,即单位值。因此没有充分的理由将其绑定到变量名。我们还可以写 let () = e 来表明我们知道这只是一个我们不关心的单位值:

let () = print_endline "Camels" in
let () = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()

But either way the boilerplate of all the let..in is annoying to have to write! So there’s a special syntax that can be used to chain together multiple functions that return unit. The expression e1; e2 first evaluates e1, which should evaluate to (), then discards that value, and evaluates e2. So we could rewrite the above code as:
但无论哪种方式,所有 let..in 的样板都必须编写起来很烦人!因此,有一种特殊的语法可用于将多个返回单元的函数链接在一起。表达式 e1; e2 首先计算 e1 ,其计算结果应为 () ,然后丢弃该值,并计算 e2 。所以我们可以将上面的代码改写为:

print_endline "Camels";
print_endline "are";
print_endline "bae"
Camels
are
bae
- : unit = ()

That is more idiomatic OCaml code, and it also looks more natural to imperative programmers.
这是更惯用的 OCaml 代码,对于命令式程序员来说也看起来更自然。

Warning 警告

There is no semicolon after the final print_endline in that example. A common mistake is to put a semicolon after each print statement. Instead, the semicolons go strictly between statements. That is, semicolon is a statement separator not a statement terminator. If you were to add a semicolon at the end, you could get a syntax error depending on the surrounding code.
在该示例中,最后的 print_endline 之后没有分号。一个常见的错误是在每个打印语句后面加一个分号。相反,分号严格位于语句之间。也就是说,分号是语句分隔符而不是语句终止符。如果您要在末尾添加分号,则可能会出现语法错误,具体取决于周围的代码。

2.6.3. Ignore 2.6.3. 忽略 ¶

If e1 does not have type unit, then e1; e2 will give a warning, because you are discarding a potentially useful value. If that is truly your intent, you can call the built-in function ignore : 'a -> unit to convert any value to ():
如果 e1 没有类型 unit ,则 e1; e2 将发出警告,因为您正在丢弃潜在有用的值。如果这确实是您的意图,您可以调用内置函数 ignore : 'a -> unit 将任何值转换为 ()

(ignore 3); 5
- : int = 5

Actually ignore is easy to implement yourself:
实际上 ignore 很容易自己实现:

let ignore x = ()
val ignore : 'a -> unit = <fun>

Or you can even write underscore to indicate the function takes in a value but does not bind that value to a name. That means the function can never use that value in its body. But that’s okay: we want to ignore it.
或者,您甚至可以编写下划线来指示该函数接受一个值,但不将该值绑定到名称。这意味着该函数永远不能在其主体中使用该值。但这没关系:我们想忽略它。

let ignore _ = ()
val ignore : 'a -> unit = <fun>

2.6.4. Printf 2.6.4. 格式化打印 ¶

For complicated text outputs, using the built-in functions for primitive type printing quickly becomes tedious. For example, suppose you wanted to write a function to print a statistic:
对于复杂的文本输出,使用内置功能进行原始类型打印很快就会变得乏味。例如,假设您想编写一个函数来打印统计信息:

(** [print_stat name num] prints [name: num]. *)
let print_stat name num =
  print_string name;
  print_string ": ";
  print_float num;
  print_newline ()
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()

How could we shorten print_stat? In Java you might use the overloaded + operator to turn all objects into strings:
我们如何缩短 print_stat ?在 Java 中,您可以使用重载的 + 运算符将所有对象转换为字符串:

void print_stat(String name, double num) {
   System.out.println(name + ": " + num);
}

But OCaml values are not objects, and they do not have a toString() method they inherit from some root Object class. Nor does OCaml permit overloading of operators.
但 OCaml 值不是对象,并且它们没有从某些根 Object 类继承的 toString() 方法。 OCaml 也不允许运算符重载。

Long ago though, FORTRAN invented a different solution that other languages like C and Java and even Python support. The idea is to use a format specifier to —as the name suggest— specify how to format output. The name this idea is best known under is probably “printf”, which refers to the name of the C library function that implemented it. Many other languages and libraries still use that name, including OCaml’s Printf module.
但很久以前,FORTRAN 发明了一种不同的解决方案,其他语言(如 C 和 Java,甚至 Python)都支持。这个想法是使用格式说明符(顾名思义)来指定如何格式化输出。这个想法最广为人知的名字可能是“printf”,它指的是实现它的 C 库函数的名称。许多其他语言和库仍然使用该名称,包括 OCaml 的 Printf 模块。

Here’s how we’d use printf to re-implement print_stat:
以下是我们如何使用 printf 重新实现 print_stat

let print_stat name num =
  Printf.printf "%s: %F\n%!" name num
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()

The first argument to function Printf.printf is the format specifier. It looks like a string, but there’s more to it than that. It’s actually understood by the OCaml compiler in quite a deep way. Inside the format specifier there are:
函数 Printf.printf 的第一个参数是格式说明符。它看起来像一根绳子,但它的意义远不止于此。实际上 OCaml 编译器可以非常深入地理解它。格式说明符内有:

  • plain characters, and 普通字符,以及

  • conversion specifiers, which begin with %.
    转换说明符,以 % 开头。

There are about two dozen conversion specifiers available, which you can read about in the documentation of Printf. Let’s pick apart the format specifier above as an example.
大约有两打可用的转换说明符,您可以在 Printf 的文档中阅读。让我们以上面的格式说明符为例。

  • It starts with "%s", which is the conversion specifier for strings. That means the next argument to printf must be a string, and the contents of that string will be output.
    它以 "%s" 开头,这是字符串的转换说明符。这意味着 printf 的下一个参数必须是 string ,并且该字符串的内容将被输出。

  • It continues with ": ", which are just plain characters. Those are inserted into the output.
    它继续 ": " ,它们只是普通字符。这些被插入到输出中。

  • It then has another conversion specifier, %F. That means the next argument of printf must have type float, and will be output in the same format that OCaml uses to print floats.
    然后它有另一个转换说明符 %F 。这意味着 printf 的下一个参数必须具有类型 float ,并且将以 OCaml 用于打印浮点数的相同格式输出。

  • The newline "\n" after that is another plain character sequence.
    之后的换行符 "\n" 是另一个纯字符序列。

  • Finally the conversion specifier "%!" means to flush the output buffer. As you might have learned in earlier programming classes, output is often buffered, meaning that it doesn’t all happen at once or right away. Flushing the buffer ensures that anything still sitting in the buffer gets output immediately. This specifier is special in that it doesn’t actually need another argument to printf.
    最后,转换说明符 "%!" 表示刷新输出缓冲区。正如您在早期的编程课程中可能已经了解到的那样,输出通常是缓冲的,这意味着它不会立即或立即发生。刷新缓冲区可确保仍然位于缓冲区中的任何内容立即得到输出。这个说明符的特殊之处在于它实际上不需要 printf 的另一个参数。

If the type of an argument is incorrect with respect to the conversion specifier, OCaml will detect that. Let’s add a type annotation to force num to be an int, and see what happens with the float conversion specifier %F:
如果参数的类型相对于转换说明符不正确,OCaml 将检测到这一点。让我们添加一个类型注释来强制 num 成为 int ,并看看浮点转换说明符 %F 会发生什么:

let print_stat name (num : int) =
  Printf.printf "%s: %F\n%!" name num
File "[14]", line 2, characters 34-37:
2 |   Printf.printf "%s: %F\n%!" name num
                                      ^^^
Error: This expression has type int but an expression was expected of type
         float

To fix that, we can change to the conversion specifier for int, which is %i:
为了解决这个问题,我们可以更改为 int 的转换说明符,即 %i

let print_stat name num =
  Printf.printf "%s: %i\n%!" name num
val print_stat : string -> int -> unit = <fun>

Another very useful variant of printf is sprintf, which collects the output in string instead of printing it:
printf 的另一个非常有用的变体是 sprintf ,它以字符串形式收集输出而不是打印它:

let string_of_stat name num =
  Printf.sprintf "%s: %F" name num
val string_of_stat : string -> float -> string = <fun>
string_of_stat "mean" 84.39
- : string = "mean: 84.39"

2.7. Debugging 2.7. 调试 ¶

Debugging is a last resort when everything else has failed. Let’s take a step back and think about everything that comes before debugging.
当其他一切都失败时,调试是最后的手段。让我们退后一步,想想调试之前发生的一切。

2.7.1. Defenses against Bugs
2.7.1. 防御错误 ¶

According to Rob Miller, there are four defenses against bugs:
根据 Rob Miller 的说法,针对错误有四种防御措施:

  1. The first defense against bugs is to make them impossible.
    对付 bug 的第一道防线就是让它们变得不可能。

    Entire classes of bugs can be eradicated by choosing to program in languages that guarantee memory safety (that no part of memory can be accessed except through a pointer (or reference) that is valid for that region of memory) and type safety (that no value can be used in a way inconsistent with its type). The OCaml type system, for example, prevents programs from buffer overflows and meaningless operations (like adding a boolean to a float), whereas the C type system does not.
    通过选择使用保证内存安全(除了通过对该内存区域有效的指针(或引用)之外,无法访问内存的任何部分)和类型安全(没有值)的语言进行编程,可以消除整个类别的错误可以以与其类型不一致的方式使用)。例如,OCaml 类型系统可以防止程序发生缓冲区溢出和无意义的操作(例如向浮点数添加布尔值),而 C 类型系统则不能。

  2. The second defense against bugs is to use tools that find them.
    针对错误的第二个防御措施是使用发现错误的工具。

    There are automated source-code analysis tools, like FindBugs, which can find many common kinds of bugs in Java programs, and SLAM, which is used to find bugs in device drivers. The subfield of CS known as formal methods studies how to use mathematics to specify and verify programs, that is, how to prove that programs have no bugs. We’ll study verification later in this course.
    有一些自动源代码分析工具,例如 FindBugs,它可以查找 Java 程序中的许多常见错误,以及 SLAM,它用于查找设备驱动程序中的错误。 CS的子领域称为形式化方法,研究如何使用数学来指定和验证程序,即如何证明程序没有错误。我们将在本课程的后面部分研究验证。

    Social methods such as code reviews and pair programming are also useful tools for finding bugs. Studies at IBM in the 1970s-1990s suggested that code reviews can be remarkably effective. In one study (Jones, 1991), code inspection found 65% of the known coding errors and 25% of the known documentation errors, whereas testing found only 20% of the coding errors and none of the documentation errors.
    代码审查和结对编程等社交方法也是查找错误的有用工具。 IBM 在 20 世纪 70 年代至 90 年代的研究表明,代码审查非常有效。在一项研究中(Jones,1991),代码检查发现了 65% 的已知编码错误和 25% 的已知文档错误,而测试仅发现了 20% 的编码错误,并且没有发现任何文档错误。

  3. The third defense against bugs is to make them immediately visible.
    针对错误的第三种防御措施是让它们立即可见。

    The earlier a bug appears, the easier it is to diagnose and fix. If computation instead proceeds past the point of the bug, then that further computation might obscure where the failure really occurred. Assertions in the source code make programs “fail fast” and “fail loudly”, so that bugs appear immediately, and the programmer knows exactly where in the source code to look.
    错误出现得越早,诊断和修复就越容易。如果计算继续进行到错误点之后,那么进一步的计算可能会掩盖故障真正发生的位置。源代码中的断言使程序“快速失败”和“大声失败”,因此错误立即出现,并且程序员确切地知道要在源代码中查找何处。

  4. The fourth defense against bugs is extensive testing.
    针对错误的第四种防御措施是广泛的测试。

    How can you know whether a piece of code has a particular bug? Write tests that would expose the bug, then confirm that your code doesn’t fail those tests. Unit tests for a relatively small piece of code, such as an individual function or module, are especially important to write at the same time as you develop that code. Running of those tests should be automated, so that if you ever break the code, you find out as soon as possible. (That’s really Defense 3 again.)
    如何知道一段代码是否存在特定的错误?编写会暴露错误的测试,然后确认您的代码不会失败这些测试。在开发代码的同时编写相对较小的代码(例如单个函数或模块)的单元测试尤其重要。这些测试的运行应该是自动化的,这样如果你破坏了代码,你就能尽快发现。 (这又是《防御战 3》。)

After all those defenses have failed, a programmer is forced to resort to debugging.
当所有这些防御措施都失败后,程序员被迫诉诸调试。

2.7.2. How to Debug
2.7.2. 如何调试 ¶

So you’ve discovered a bug. What next?
所以你发现了一个错误。接下来是什么?

  1. Distill the bug into a small test case. Debugging is hard work, but the smaller the test case, the more likely you are to focus your attention on the piece of code where the bug lurks. Time spent on this distillation can therefore be time saved, because you won’t have to re-read lots of code. Don’t continue debugging until you have a small test case!
    将错误提炼成一个小测试用例。调试是一项艰苦的工作,但是测试用例越小,您就越有可能将注意力集中在错误潜伏的代码片段上。因此,花在这种提炼上的时间可以节省时间,因为您不必重新阅读大量代码。在获得一个小测试用例之前,不要继续调试!

  2. Employ the scientific method. Formulate a hypothesis as to why the bug is occurring. You might even write down that hypothesis in a notebook, as if you were in a Chemistry lab, to clarify it in your own mind and keep track of what hypotheses you’ve already considered. Next, design an experiment to affirm or deny that hypothesis. Run your experiment and record the result. Based on what you’ve learned, reformulate your hypothesis. Continue until you have rationally, scientifically determined the cause of the bug.
    采用科学方法。提出关于错误发生原因的假设。您甚至可以在笔记本上写下该假设,就像在化学实验室一样,以便在自己的头脑中澄清它并跟踪您已经考虑过的假设。接下来,设计一个实验来肯定或否定该假设。运行你的实验并记录结果。根据您所学到的知识,重新阐述您的假设。继续下去,直到您理性、科学地确定错误的原因。

  3. Fix the bug. The fix might be a simple correction of a typo. Or it might reveal a design flaw that causes you to make major changes. Consider whether you might need to apply the fix to other locations in your code base—for example, was it a copy and paste error? If so, do you need to refactor your code?
    修复错误。修复可能是对拼写错误的简单更正。或者它可能会揭示一个设计缺陷,导致您做出重大改变。考虑是否需要将修复应用到代码库中的其他位置,例如,是否是复制和粘贴错误?如果是这样,您是否需要重构您的代码?

  4. Permanently add the small test case to your test suite. You wouldn’t want the bug to creep back into your code base. So keep track of that small test case by keeping it as part of your unit tests. That way, any time you make future changes, you will automatically be guarding against that same bug. Repeatedly running tests distilled from previous bugs is a part of regression testing.
    将小测试用例永久添加到您的测试套件中。您不希望该错误重新进入您的代码库。因此,通过将其作为单元测试的一部分来跟踪这个小测试用例。这样,每当您将来进行更改时,您都会自动防范相同的错误。重复运行从以前的错误中提取的测试是回归测试的一部分。

2.7.3. Debugging in OCaml
2.7.3. 在 OCaml 中调试 ¶

Here are a couple tips on how to debug—if you are forced into it—in OCaml.
这里有一些关于如何在 OCaml 中进行调试(如果您被迫这样做)的提示。

  • Print statements. Insert a print statement to ascertain the value of a variable. Suppose you want to know what the value of x is in the following function:
    打印报表。插入打印语句以确定变量的值。假设您想知道以下函数中 x 的值是多少:

    let inc x = x + 1
    

    Just add the line below to print that value:
    只需添加以下行即可打印该值:

    let inc x =
      let () = print_int x in
      x + 1
    
  • Function traces. Suppose you want to see the trace of recursive calls and returns for a function. Use the #trace directive:
    功能痕迹。假设您想查看函数的递归调用和返回的跟踪。使用 #trace 指令:

    # let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);;
    # #trace fib;;
    

    If you evaluate fib 2, you will now see the following output:
    如果您评估 fib 2 ,您现在将看到以下输出:

    fib <-- 2
    fib <-- 0
    fib --> 1
    fib <-- 1
    fib --> 1
    fib --> 2
    

    To stop tracing, use the #untrace directive.
    要停止跟踪,请使用 #untrace 指令。

  • Debugger. OCaml has a debugging tool ocamldebug. You can find a tutorial on the OCaml website. Unless you are using Emacs as your editor, you will probably find this tool to be harder to use than just inserting print statements.
    调试器。 OCaml 有一个调试工具 ocamldebug 。您可以在 OCaml 网站上找到教程。除非您使用 Emacs 作为编辑器,否则您可能会发现这个工具比仅仅插入打印语句更难使用。

2.7.4. Defensive Programming
2.7.4. 防御性编程 ¶

As we discussed earlier in the section on debugging, one defense against bugs is to make any bugs (or errors) immediately visible. That idea connects with idea of preconditions.
正如我们前面在调试部分中讨论的那样,针对错误的一种防御措施是使任何错误(或错误)立即可见。这个想法与先决条件的想法有关。

Consider this specification of random_int:
考虑 random_int 的这个规范:

(** [random_int bound] is a random integer between 0 (inclusive)
    and [bound] (exclusive).  Requires: [bound] is greater than 0
    and less than 2^30. *)

If the client of random_int passes a value of bound that violates the “Requires” clause, such as -1, the implementation of random_int is free to do anything whatsoever. All bets are off when the client violates the precondition.
如果 random_int 的客户端传递了违反“Requires”子句的 bound 值,例如 -1 ,则执行 random_int 可以自由地做任何事。当客户违反先决条件时,所有赌注都会失败。

But the most helpful thing for random_int to do is to immediately expose the fact that the precondition was violated. After all, chances are that the client didn’t mean to violate it.
但对 random_int 来说最有帮助的事情就是立即揭露违反前提条件的事实。毕竟,客户很可能无意违反它。

So the implementor of random_int would do well to check whether the precondition is violated, and if so, raise an exception. Here are three possibilities of that kind of defensive programming:
因此 random_int 的实现者最好检查是否违反前提条件,如果违反,则引发异常。以下是这种防御性编程的三种可能性:

(* possibility 1 *)
let random_int bound =
  assert (bound > 0 && bound < 1 lsl 30);
  (* proceed with the implementation of the function *)

(* possibility 2 *)
let random_int bound =
  if not (bound > 0 && bound < 1 lsl 30)
  then invalid_arg "bound";
  (* proceed with the implementation of the function *)

(* possibility 3 *)
let random_int bound =
  if not (bound > 0 && bound < 1 lsl 30)
  then failwith "bound";
  (* proceed with the implementation of the function *)

The second possibility is probably the most informative to the client, because it uses the built-in function invalid_arg to raise the well-named exception Invalid_argument. In fact, that’s exactly what the standard library implementation of this function does.
第二种可能性可能对客户端来说信息量最大,因为它使用内置函数 invalid_arg 来引发名称良好的异常 Invalid_argument 。事实上,这正是该函数的标准库实现的作用。

The first possibility is probably most useful when you are trying to debug your own code, rather than choosing to expose a failed assertion to a client.
当您尝试调试自己的代码而不是选择向客户端公开失败的断言时,第一种可能性可能最有用。

The third possibility differs from the second only in the name (Failure) of the exception that is raised. It might be useful in situations where the precondition involves more than just a single invalid argument.
第三种可能性与第二种的不同之处仅在于引发的异常的名称 ( Failure )。在前提条件涉及多个无效参数的情况下,它可能很有用。

In this example, checking the precondition is computationally cheap. In other cases, it might require a lot of computation, so the implementer of the function might prefer not to check the precondition, or only to check some inexpensive approximation to it.
在此示例中,检查前提条件的计算成本很低。在其他情况下,它可能需要大量计算,因此函数的实现者可能宁愿不检查前提条件,或者只检查一些廉价的近似值。

Sometimes programmers worry unnecessarily that defensive programming will be too expensive—either in terms of the time it costs them to implement the checks initially, or in the run-time costs that will be paid in checking assertions. These concerns are far too often misplaced. The time and money it costs society to repair faults in software suggests that we could all afford to have programs that run a little more slowly.
有时,程序员不必要地担心防御性编程会太昂贵——无论是在最初实施检查所花费的时间方面,还是在检查断言时所付出的运行时成本方面。这些担忧常常是错误的。社会修复软件故障所花费的时间和金钱表明,我们都可以负担得起运行速度稍慢的程序。

Finally, the implementer might even choose to eliminate the precondition and restate it as a postcondition:
最后,实施者甚至可能选择消除前置条件并将其重新表述为后置条件:

(** [random_int bound] is a random integer between 0 (inclusive)
    and [bound] (exclusive).  Raises: [Invalid_argument "bound"]
    unless [bound] is greater than 0 and less than 2^30. *)

Now instead of being free to do whatever when bound is too big or too small, random_int must raise an exception. For this function, that’s probably the best choice.
现在,当 bound 太大或太小时, random_int 必须引发异常,而不是自由地执行任何操作。对于这个功能,这可能是最好的选择。

In this course, we’re not going to force you to program defensively. But if you’re savvy, you’ll start (or continue) doing it anyway. The small amount of time you spend coding up such defenses will save you hours of time in debugging, making you a more productive programmer.
在本课程中,我们不会强迫您进行防御性编程。但如果你很精明,你无论如何都会开始(或继续)这样做。您花在编码此类防御上的少量时间将为您节省数小时的调试时间,使您成为一名更有生产力的程序员。

2.8. Summary 2.8. 小结 ¶

Syntax and semantics are a powerful paradigm for learning a programming language. As we learn the features of OCaml, we’re being careful to write down their syntax and semantics. We’ve seen that there can be multiple syntaxes for expressing the same semantic idea, that is, the same computation.
语法和语义是学习编程语言的强大范例。当我们学习 OCaml 的功能时,我们会小心地写下它们的语法和语义。我们已经看到,可以有多种语法来表达相同的语义思想,即相同的计算。

The semantics of function application is the very heart of OCaml and of functional programming, and it’s something we will come back to several times throughout the course to deepen our understanding.
函数应用的语义是 OCaml 和函数式编程的核心,我们将在整个课程中多次回顾它以加深我们的理解。

2.8.1. Terms and Concepts
2.8.1. 术语和概念 ¶

  • anonymous functions 匿名函数

  • assertions 断言

  • binding 捆绑

  • binding expression 结合表达

  • body expression 身体表情

  • debugging 调试

  • defensive programming 防御性编程

  • definitions 定义

  • documentation 文档

  • dynamic semantics 动态语义

  • evaluation 评估

  • expressions 表达式

  • function application 功能应用

  • function definitions 函数定义

  • identifiers 身份标识

  • idioms 成语

  • if expressions if 表达式

  • lambda expressions lambda 表达式

  • let definition 让定义

  • let expression 让表达式

  • libraries 图书馆

  • metavariables 元变量

  • mutual recursion 相互递归

  • pipeline operator 管道操作员

  • postcondition 后置条件

  • precondition 前提

  • printing 印刷

  • recursion 递归

  • semantics 语义

  • static semantics 静态语义

  • substitution 代换

  • syntax 句法

  • tools 工具

  • type checking 类型检查

  • type inference 类型推断

  • values 价值观

2.8.2. Further Reading 2.8.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapter 3
    Objective Caml 简介,第 3 章

  • OCaml from the Very Beginning, chapter 2
    OCaml 从头开始​​,第 2 章

  • Real World OCaml, chapter 2
    现实世界 OCaml,第 2 章

  • Tail Recursion, The Musical. Tail-call optimization explained in the context of JavaScript with cute 8-bit animations, and Disney songs!
    尾递归,音乐剧。在 JavaScript 上下文中用可爱的 8 位动画和迪士尼歌曲解释了尾部调用优化!

2.9. Exercises 2.9. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: values [★] 练习:值[★]

What is the type and value of each of the following OCaml expressions?
以下每个 OCaml 表达式的类型和值是什么?

  • 7 * (1 + 2 + 3)

  • "CS " ^ string_of_int 3110

Hint: type each expression into the toplevel and it will tell you the answer. Note: ^ is not exponentiation.
提示:在顶层输入每个表达式,它会告诉你答案。注意: ^ 不是求幂。


Exercise: operators [★★] 练习:运算符 [★★]

Examine the table of all operators in the OCaml manual (you will have to scroll down to find it on that page).
检查 OCaml 手册中所有运算符的表(您必须向下滚动才能在该页面上找到它)。

  • Write an expression that multiplies 42 by 10.
    编写一个将 42 乘以 10 的表达式。

  • Write an expression that divides 3.14 by 2.0. Hint: integer and floating-point operators are written differently in OCaml.
    编写一个将 3.14 除以 2.0 的表达式。提示:整数和浮点运算符在 OCaml 中的写法不同。

  • Write an expression that computes 4.2 raised to the seventh power. Note: there is no built-in integer exponentiation operator in OCaml (nor is there in C, by the way), in part because it is not an operation provided by most CPUs.
    编写一个表达式来计算 4.2 的七次方。注意:OCaml 中没有内置的整数求幂运算符(顺便说一句,C 中也没有),部分原因是它不是大多数 CPU 提供的运算。


Exercise: equality [★] 练习:相等性[★]

  • Write an expression that compares 42 to 42 using structural equality.
    编写一个使用结构相等性比较 4242 的表达式。

  • Write an expression that compares "hi" to "hi" using structural equality. What is the result?
    编写一个使用结构相等性比较 "hi""hi" 的表达式。结果是什么?

  • Write an expression that compares "hi" to "hi" using physical equality. What is the result?
    编写一个使用物理相等性比较 "hi""hi" 的表达式。结果是什么?


Exercise: assert [★] 练习:断言[★]

  • Enter  进入assert true;; into utop and see what happens.
    进入 utop 看看会发生什么。

  • Enter  进入assert false;; into utop and see what happens.
    进入 utop 看看会发生什么。

  • Write an expression that asserts 2110 is not (structurally) equal to 3110.
    编写一个表达式,断言 2110(在结构上)不等于 3110。


Exercise: if [★] 练习:条件表达式[★]

Write an if expression that evaluates to
编写一个 if 表达式,其计算结果为
42 if 2 is greater than
大于
1 and otherwise evaluates to
否则评估为
7.
编写一个 if 表达式,如果 2 大于 1 ,则计算结果为 42 ,否则计算结果为 7


Exercise: double fun [★] 锻炼:加倍函数[★]

Using the increment function from above as a guide, define a function
定义一个函数
double that multiplies its input by 2. For example, double 7 would be 14. Test your function by applying it to a few inputs. Turn those test cases into assertions.
使用上面的增量函数作为指导,定义一个将其输入乘以 2 的函数 double 。例如, double 7 将是 14 。通过将其应用于一些输入来测试您的函数。将这些测试用例转化为断言。


Exercise: more fun [★★] 锻炼:更多函数[★★]

  • Define a function that computes the cube of a floating-point number. Test your function by applying it to a few inputs.
    定义一个计算浮点数立方的函数。通过将其应用于一些输入来测试您的函数。

  • Define a function that computes the sign (1, 0, or -1) of an integer. Use a nested if expression. Test your function by applying it to a few inputs.
    定义一个计算整数符号(1、0 或 -1)的函数。使用嵌套的 if 表达式。通过将其应用于一些输入来测试您的函数。

  • Define a function that computes the area of a circle given its radius. Test your function with
    定义一个函数,计算给定半径的圆的面积。测试你的功能
    assert.
    定义一个函数,计算给定半径的圆的面积。使用 assert 测试您的函数。

For the latter, bear in mind that floating-point arithmetic is not exact. Instead of asserting an exact value, you should assert that the result is “close enough”, e.g., within 1e-5. If that’s unfamiliar to you, it would be worthwhile to read up on floating-point arithmetic.
对于后者,请记住浮点运算并不精确。您应该断言结果“足够接近”,例如在 1e-5 之内,而不是断言精确值。如果您不熟悉,那么值得阅读浮点运算。

A function that take multiple inputs can be defined just by providing additional names for those inputs as part of the let definition. For example, the following function computes the average of three arguments:
只需在 let 定义中为这些输入提供附加名称即可定义采用多个输入的函数。例如,以下函数计算三个参数的平均值:

let avg3 x y z = (x +. y +. z) /. 3.

Exercise: RMS [★★] 练习:RMS [★★]

Define a function 定义一个函数 that computes the root mean square of two numbers—i.e., (x2+y2)/2. Test your function with assert.
定义一个计算两个数字的均方根的函数,即 (x2+y2)/2 。使用 assert 测试您的函数。


Exercise: date fun [★★★] 练习:日期函数[★★★]

Define a function 定义一个函数 that takes an integer d and string m as input and returns true just when d and m form a valid date. Here, a valid date has a month that is one of the following abbreviations: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sept, Oct, Nov, Dec. And the day must be a number that is between 1 and the minimum number of days in that month, inclusive. For example, if the month is Jan, then the day is between 1 and 31, inclusive, whereas if the month is Feb, then the day is between 1 and 28, inclusive.
定义一个函数,将整数 d 和字符串 m 作为输入,并在 dm > 形成有效日期。此处,有效日期的月份为以下缩写之一:Jan、Feb、Mar、Apr、May、Jun、Jul、Aug、Sept、Oct、Nov、Dec。并且日期必须是介于1 以及该月的最少天数(含)。例如,如果月份是 1 月,则日期在 1 到 31 之间(含),而如果月份是 2 月,则日期在 1 到 28 之间(含)。

How terse (i.e., few and short lines of code) can you make your function? You can definitely do this in fewer than 12 lines.
你的函数能有多简洁(即几行短代码)?您绝对可以用不到 12 行代码来完成此操作。


Exercise: fib [★★] 练习:斐波那契[★★]

Define a recursive function
定义递归函数
fib : int -> int, such that  ,使得fib n is the  是个nth number in the
中的第
Fibonacci sequence 斐波那契数列, which is 1, 1, 2, 3, 5, 8, 13, … That is:
,即 1, 1, 2, 3, 5, 8, 13, … 即:

  • fib 1 = 1,  fib 1 = 1

  • fib 2 = 1, and  fib 2 = 1

  • fib n = fib (n-1) + fib (n-2) for any n > 2.
    fib n = fib (n-1) + fib (n-2) 对于任何 n > 2

Test your function in the toplevel.
在顶层测试您的功能。


Exercise: fib fast [★★★] 练习:斐波那契快速[★★★]

How quickly does your implementation of fib compute the 50th Fibonacci number? If it computes nearly instantaneously, congratulations! But the recursive solution most people come up with at first will seem to hang indefinitely. The problem is that the obvious solution computes subproblems repeatedly. For example, computing fib 5 requires computing both fib 3 and fib 4, and if those are computed separately, a lot of work (an exponential amount, in fact) is being redone.
您的 fib 实现计算第 50 个斐波那契数的速度有多快?如果它几乎是即时计算的,那么恭喜!但大多数人一开始提出的递归解决方案似乎会无限期地挂起。问题是显而易见的解决方案会重复计算子问题。例如,计算 fib 5 需要同时计算 fib 3fib 4 ,如果分别计算它们,则需要大量工作(实际上是指数量)正在重做。

Create a function  创建一个函数fib_fast that requires only a linear amount of work. Hint: write a recursive helper function h : int -> int -> int -> int, where h n pp p is defined as follows:
创建一个仅需要线性工作量的函数 fib_fast 。提示:编写一个递归辅助函数 h : int -> int -> int -> int ,其中 h n pp p 定义如下:

  • h 1 pp p = p, and  h 1 pp p = p

  • h n pp p = h (n-1) p (pp+p) for any n > 1.
    h n pp p = h (n-1) p (pp+p) 对于任何 n > 1

The idea of h is that it assumes the previous two Fibonacci numbers were pp and p, then computes forward n more numbers. Hence, fib n = h n 0 1 for any n > 0.
h 的想法是,它假设前两个斐波那契数是 ppp ,然后向前计算 n 个数字。因此, fib n = h n 0 1 对于任何 n > 0

What is the first value of
第一个值是多少
n for which fib_fast n is negative, indicating that integer overflow occurred?
n 中第一个 fib_fast n 为负数的值是多少,表明发生了整数溢出?


Exercise: poly types [★★★]
练习:多类型[★★★]

What is the type of each of the functions below? You can ask the toplevel to check your answers
下面每个函数的类型是什么?你可以要求高层检查你的答案

let f x = if x then x else x
let g x y = if y then x else x
let h x y z = if x then y else z
let i x y z = if x then y else y

Exercise: divide [★★] 练习:除法[★★]

Write a function 写一个函数 divide : numerator:float -> denominator:float -> float. Apply your function.
编写一个函数 divide : numerator:float -> denominator:float -> float 。应用你的函数。


Exercise: associativity [★★]
练习:关联性 [★★]

Suppose that we have defined let add x y = x + y. Which of the following produces an integer, which produces a function, and which produces an error? Decide on an answer, then check your answer in the toplevel.
假设我们已经定义了 let add x y = x + y 。下面哪一个产生一个整数,哪个产生一个函数,哪个产生一个错误?决定一个答案,然后在顶层检查你的答案。

  • add 5 1

  • add 5

  • (add 5) 1

  • add (5 1)


Exercise: average [★★] 练习:平均[★★]

Define an infix operator
定义中缀运算符
+/. to compute the average of two floating-point numbers. For example,
计算两个浮点数的平均值。例如,

  • 1.0 +/. 2.0 = 1.5

  • 0. +/. 0. = 0.


Exercise: hello world [★]
练习:你好世界[★]

Type the following in utop:
在 utop 中输入以下内容:

  • print_endline "Hello world!";;

  • print_string "Hello world!";;

Notice the difference in output from each.
请注意每个输出的差异。

3. Data and Types
3. 数据和类型 ¶

In this chapter, we’ll examine some of OCaml’s built-in data types, including lists, variants, records, tuples, and options. Many of those are likely to feel familiar from other programming languages. In particular,
在本章中,我们将研究 OCaml 的一些内置数据类型,包括列表、变体、记录、元组和选项。其中许多可能与其他编程语言相似。尤其,

  • lists and tuples, might feel similar to Python; and
    列表和元组,可能感觉与 Python 类似;和

  • records and variants, might feel similar to struct and enum types from C or Java.
    记录和变体,可能感觉类似于 C 或 Java 中的 structenum 类型。

Because of that familiarity, we call these standard data types. We’ll learn about pattern matching, which is a feature that’s less likely to be familiar.
由于这种熟悉性,我们将这些称为标准数据类型。我们将学习模式匹配,这是一个不太熟悉的功能。

Almost immediately after we learn about lists, we’ll pause our study of standard data types to learn about unit testing in OCaml with OUnit, a unit testing framework similar to those you might have used in other languages. OUnit relies on lists, which is why we couldn’t cover it before now.
几乎在我们了解列表之后,我们将暂停对标准数据类型的研究,以了解 OCaml 中使用 OUnit 的单元测试,这是一个与您可能在其他语言中使用过的单元测试框架类似的单元测试框架。 OUnit 依赖于列表,这就是为什么我们之前无法介绍它。

Later in the chapter, we study some OCaml data types that are unlikely to be as familiar from other languages. They include:
在本章后面,我们将研究一些其他语言不太熟悉的 OCaml 数据类型。他们包括:

  • options, which are loosely related to null in Java;
    可选者,与 Java 中的 null 松散相关;

  • association lists, which are an amazingly simple implementation of maps (aka dictionaries) based on lists and tuples;
    关联列表,它是基于列表和元组的地图(又名字典)的极其简单的实现;

  • algebraic data types, which are arguably the most important kind of type in OCaml, and indeed are the power behind many of the other built-in types; and
    代数数据类型,可以说是 OCaml 中最重要的类型,实际上是许多其他内置类型背后的力量;

  • exceptions, which are a special kind of algebraic data type.
    以及,例外,这是一种特殊的代数数据类型。

3.1. Lists 3.1. 列表 ¶

An OCaml list is a sequence of values all of which have the same type. They are implemented as singly-linked lists. These lists enjoy a first-class status in the language: there is special support for easily creating and working with lists. That’s a characteristic that OCaml shares with many other functional languages. Mainstream imperative languages, like Python, have such support these days too. Maybe that’s because programmers find it so pleasant to work directly with lists as a first-class part of the language, rather than having to go through a library (as in C and Java).
OCaml 列表是一系列具有相同类型的值。它们被实现为单链表。这些列表在该语言中享有一流的地位:为轻松创建和使用列表提供特殊支持。这是 OCaml 与许多其他函数式语言共有的特征。主流命令式语言,例如 Python,现在也有这样的支持。也许这是因为程序员发现直接使用列表作为语言的一流部分是非常愉快的,而不是必须通过库(如在 C 和 Java 中)。

3.1.1. Building Lists 3.1.1. 构建列表 ¶

Syntax. There are three syntactic forms for building lists:
句法。构建列表有三种语法形式:

[]
e1 :: e2
[e1; e2; ...; en]

The empty list is written [] and is pronounced “nil”, a name that comes from Lisp. Given a list lst and element elt, we can prepend elt to lst by writing elt :: lst. The double-colon operator is pronounced “cons”, a name that comes from an operator in Lisp that constructs objects in memory. “Cons” can also be used as a verb, as in “I will cons an element onto the list.” The first element of a list is usually called its head and the rest of the elements (if any) are called its tail.
空列表写作 [] ,发音为“nil”,这个名字来自Lisp。给定列表 lst 和元素 elt ,我们可以通过编写 elt :: lstlst 前面添加 elt 。双冒号运算符发音为“cons”,这个名称来自 Lisp 中用于在内存中构造对象的运算符。 “Cons”也可以用作动词,如“我将把一个元素添加到列表中”。列表的第一个元素通常称为表头,其余元素(如果有)称为表尾。

The square bracket syntax is convenient but unnecessary. Any list [e1; e2; ...; en] could instead be written with the more primitive nil and cons syntax: e1 :: e2 :: ... :: en :: []. When a pleasant syntax can be defined in terms of a more primitive syntax within the language, we call the pleasant syntax syntactic sugar: it makes the language “sweeter”. Transforming the sweet syntax into the more primitive syntax is called desugaring.
方括号语法很方便,但没有必要。任何列表 [e1; e2; ...; en] 都可以用更原始的 nil 和 cons 语法来编写: e1 :: e2 :: ... :: en :: [] 。当可以用语言中更原始的语法来定义令人愉快的语法时,我们将令人愉快的语法称为语法糖:它使语言“更甜蜜”。将甜蜜语法转换为更原始的语法称为脱糖。

Because the elements of the list can be arbitrary expressions, lists can be nested as deeply as we like, e.g., [[[]]; [[1; 2; 3]]].
因为列表的元素可以是任意表达式,所以列表可以嵌套任意深度,例如 [[[]]; [[1; 2; 3]]]

Dynamic semantics. 动态语义。

  • [] is already a value.
    [] 已经是一个值。

  • If e1 evaluates to v1, and if e2 evaluates to v2, then e1 :: e2 evaluates to v1 :: v2.
    如果 e1 计算结果为 v1 ,并且 e2 计算结果为 v2 ,则 e1 :: e2 计算结果为 v1 :: v2

As a consequence of those rules and how to desugar the square-bracket notation for lists, we have the following derived rule:
根据这些规则以及如何对列表的方括号表示法进行脱糖,我们得出以下派生规则:

  • If ei evaluates to vi for all i in 1..n, then [e1; ...; en] evaluates to [v1; ...; vn].
    如果 1..n 中所有 iei 计算结果为 vi ,则 [e1; ...; en] 计算结果为 [v1; ...; vn]

It’s starting to get tedious to write “evaluates to” in all our evaluation rules. So let’s introduce a shorter notation for it. We’ll write e ==> v to mean that e evaluates to v. Note that ==> is not a piece of OCaml syntax. Rather, it’s a notation we use in our description of the language, kind of like metavariables. Using that notation, we can rewrite the latter two rules above:
在我们所有的评估规则中编写“评估”开始变得乏味。那么让我们为它引入一个更短的符号。我们将编写 e ==> v 来表示 e 的计算结果为 v 。请注意, ==> 不是 OCaml 语法的一部分。相反,它是我们在语言描述中使用的符号,有点像元变量。使用该表示法,我们可以重写上面的后两个规则:

  • If e1 ==> v1, and if e2 ==> v2, then e1 :: e2 ==> v1 :: v2.
    如果 e1 ==> v1 ,并且如果 e2 ==> v2 ,则 e1 :: e2 ==> v1 :: v2

  • If ei ==> vi for all i in 1..n, then [e1; ...; en] ==> [v1; ...; vn].
    如果 ei ==> vi 代表 1..n 中的所有 i ,则 [e1; ...; en] ==> [v1; ...; vn]

Static semantics. 静态语义。

All the elements of a list must have the same type. If that element type is t, then the type of the list is t list. You should read such types from right to left: t list is a list of t’s, t list list is a list of list of t’s, etc. The word list itself here is not a type: there is no way to build an OCaml value that has type simply list. Rather, list is a type constructor: given a type, it produces a new type. For example, given int, it produces the type int list. You could think of type constructors as being like functions that operate on types, instead of functions that operate on values.
列表中的所有元素必须具有相同的类型。如果该元素类型为 t ,则列表的类型为 t list 。您应该从右到左阅读此类类型: t listt 的列表, t list listt 等。此处的单词 list 本身不是类型:无法构建简单类型 list 的 OCaml 值。相反, list 是一个类型构造函数:给定一个类型,它会生成一个新类型。例如,给定 int ,它会生成类型 int list 。您可以将类型构造函数视为对类型进行操作的函数,而不是对值进行操作的函数。

The type-checking rules:
型式检查规则:

  • [] : 'a list

  • If e1 : t and e2 : t list then e1 :: e2 : t list. In case the colons and their precedence is confusing, the latter means (e1 :: e2) : t list.
    如果 e1 : te2 : t liste1 :: e2 : t list 。如果冒号及其优先级令人困惑,后者意味着 (e1 :: e2) : t list

In the rule for [], recall that 'a is a type variable: it stands for an unknown type. So the empty list is a list whose elements have an unknown type. If we cons an int onto it, say 2 :: [], then the compiler infers that for that particular list, 'a must be int. But if in another place we cons a bool onto it, say true :: [], then the compiler infers that for that particular list, 'a must be bool.
[] 的规则中,请记住 'a 是一个类型变量:它代表未知类型。所以空列表是一个元素类型未知的列表。如果我们在其上添加 int ,例如 2 :: [] ,那么编译器会推断对于该特定列表, 'a 必须是 int 。但是,如果在另一个地方我们在其上添加 bool ,例如 true :: [] ,那么编译器会推断对于该特定列表, 'a 必须是 bool

3.1.2. Accessing Lists 3.1.2. 访问列表 ¶

Note 笔记

The video linked above also uses records and tuples as examples. Those are covered in a later section of this book.
上面链接的视频也使用记录和元组作为示例。这些内容将在本书的后面部分中介绍。

There are really only two ways to build a list, with nil and cons. So if we want to take apart a list into its component pieces, we have to say what to do with the list if it’s empty, and what to do if it’s non-empty (that is, a cons of one element onto some other list). We do that with a language feature called pattern matching.
构建列表实际上只有两种方法,即 nil 和 cons。因此,如果我们想要将一个列表分解为其组成部分,我们必须说明如果该列表为空则如何处理该列表,以及如果该列表非空则如何处理(即,将一个元素的缺点放到另一个列表上) )。我们通过称为模式匹配的语言功能来做到这一点。

Here’s an example of using pattern matching to compute the sum of a list:
下面是使用模式匹配计算列表总和的示例:

let rec sum lst =
  match lst with
  | [] -> 0
  | h :: t -> h + sum t
val sum : int list -> int = <fun>

This function says to take the input lst and see whether it has the same shape as the empty list. If so, return 0. Otherwise, if it has the same shape as the list h :: t, then let h be the first element of lst, and let t be the rest of the elements of lst, and return h + sum t. The choice of variable names here is meant to suggest “head” and “tail” and is a common idiom, but we could use other names if we wanted. Another common idiom is:
该函数表示获取输入 lst 并查看它是否与空列表具有相同的形状。如果是,则返回 0。否则,如果它与列表 h :: t 具有相同的形状,则令 hlst 的第一个元素,并令 tlst 的其余元素,并返回 h + sum t 。这里变量名的选择是为了暗示“头”和“尾”,这是一个常见的习惯用法,但如果我们愿意的话,我们可以使用其他名称。另一个常见的习语是:

let rec sum xs =
  match xs with
  | [] -> 0
  | x :: xs' -> x + sum xs'
val sum : int list -> int = <fun>

That is, the input list is a list of xs (pronounced EX-uhs), the head element is an x, and the tail is xs’ (pronounced EX-uhs prime).
也就是说,输入列表是 xs(发音为 EX-uhs)的列表,头元素是 x,尾部元素是 xs’(发音为 EX-uhs prime)。

Syntactically it isn’t necessary to use so many lines to define sum. We could do it all on one line:
从语法上讲,没有必要使用这么多行来定义 sum 。我们可以用一行完成这一切:

let rec sum xs = match xs with | [] -> 0 | x :: xs' -> x + sum xs'
val sum : int list -> int = <fun>

Or, noting that the first | after with is optional regardless of how many lines we use, we could also write:
或者,注意 with 之后的第一个 | 是可选的,无论我们使用多少行,我们也可以编写:

let rec sum xs = match xs with [] -> 0 | x :: xs' -> x + sum xs'
val sum : int list -> int = <fun>

The multi-line format is what we’ll usually use in this book, because it helps the human eye understand the syntax a bit better. OCaml code formatting tools, though, are moving toward the single-line format whenever the code is short enough to fit on just one line.
多行格式是我们在本书中通常使用的格式,因为它可以帮助人眼更好地理解语法。不过,只要代码足够短,只能放在一行中,OCaml 代码格式化工具就会转向单行格式。

Here’s another example of using pattern matching to compute the length of a list:
这是使用模式匹配来计算列表长度的另一个示例:

let rec length lst =
  match lst with
  | [] -> 0
  | h :: t -> 1 + length t
val length : 'a list -> int = <fun>

Note how we didn’t actually need the variable h in the right-hand side of the pattern match. When we want to indicate the presence of some value in a pattern without actually giving it a name, we can write _ (the underscore character):
请注意,我们实际上并不需要模式匹配右侧的变量 h 。当我们想要指示模式中存在某个值而不实际给它命名时,我们可以编写 _ (下划线字符):

let rec length lst =
  match lst with
  | [] -> 0
  | _ :: t -> 1 + length t
val length : 'a list -> int = <fun>

That function is actually built-in as part of the OCaml standard library List module. Its name there is List.length. That “dot” notation indicates the function named length inside the module named List, much like the dot notation used in many other languages.
该函数实际上是作为 OCaml 标准库 List 模块的一部分内置的。它的名字是 List.length 。该“点”表示法表示名为 List 的模块内名为 length 的函数,与许多其他语言中使用的点表示法非常相似。

And here’s a third example that appends one list onto the beginning of another list:
这是第三个示例,它将一个列表附加到另一个列表的开头:

let rec append lst1 lst2 =
  match lst1 with
  | [] -> lst2
  | h :: t -> h :: append t lst2
val append : 'a list -> 'a list -> 'a list = <fun>

For example, append [1; 2] [3; 4] is [1; 2; 3; 4]. That function is actually available as a built-in operator @, so we could instead write [1; 2] @ [3; 4].
例如, append [1; 2] [3; 4][1; 2; 3; 4] 。该函数实际上可用作内置运算符 @ ,因此我们可以编写 [1; 2] @ [3; 4]

As a final example, we could write a function to determine whether a list is empty:
作为最后一个示例,我们可以编写一个函数来确定列表是否为空:

let empty lst =
  match lst with
  | [] -> true
  | h :: t -> false
val empty : 'a list -> bool = <fun>

But there is a much better way to write the same function without pattern matching:
但是有一种更好的方法可以在不进行模式匹配的情况下编写相同的函数:

let empty lst =
  lst = []
val empty : 'a list -> bool = <fun>

Note how all the recursive functions above are similar to doing proofs by induction on the natural numbers: every natural number is either 0 or is 1 greater than some other natural number n, and so a proof by induction has a base case for 0 and an inductive case for n+1. Likewise all our functions have a base case for the empty list and a recursive case for the list that has one more element than another list. This similarity is no accident. There is a deep relationship between induction and recursion; we’ll explore that relationship in more detail later in the book.
请注意上面的所有递归函数与对自然数进行归纳证明有何相似之处:每个自然数要么是 0,要么比其他自然数大 1 n ,因此归纳证明有0 的基本情况和 n+1 的归纳情况。同样,我们所有的函数都有一个空列表的基本情况和一个比另一个列表多一个元素的列表的递归情况。这种相似性并非偶然。归纳法和递归之间有着很深的关系;我们将在本书后面更详细地探讨这种关系。

By the way, there are two library functions List.hd and List.tl that return the head and tail of a list. It is not good, idiomatic OCaml to apply these directly to a list. The problem is that they will raise an exception when applied to the empty list, and you will have to remember to handle that exception. Instead, you should use pattern matching: you’ll then be forced to match against both the empty list and the non-empty list (at least), which will prevent exceptions from being raised, thus making your program more robust.
顺便说一下,有两个库函数 List.hdList.tl 返回列表的头和尾。将这些直接应用到列表并不是一个好的、惯用的 OCaml 做法。问题是它们在应用于空列表时会引发异常,并且您必须记住处理该异常。相反,您应该使用模式匹配:然后您将被迫匹配空列表和非空列表(至少),这将防止引发异常,从而使您的程序更加健壮。

3.1.3. (Not) Mutating Lists
3.1.3. (不)变列表 ¶

Lists are immutable. There’s no way to change an element of a list from one value to another. Instead, OCaml programmers create new lists out of old lists. For example, suppose we wanted to write a function that returned the same list as its input list, but with the first element (if there is one) incremented by one. We could do that:
列表是不可变的。无法将列表的元素从一个值更改为另一个值。相反,OCaml 程序员从旧列表中创建新列表。例如,假设我们想要编写一个函数,返回与其输入列表相同的列表,但第一个元素(如果有)增加 1。我们可以这样做:

let inc_first lst =
  match lst with
  | [] -> []
  | h :: t -> h + 1 :: t

Now you might be concerned about whether we’re being wasteful of space. After all, there are at least two ways the compiler could implement the above code:
现在您可能会担心我们是否浪费空间。毕竟,编译器至少有两种方法可以实现上述代码:

  1. Copy the entire tail list t when the new list is created in the pattern match with cons, such that the amount of memory in use just increased by an amount proportionate to the length of t.
    当在与 cons 的模式匹配中创建新列表时,复制整个尾部列表 t ,使得使用的内存量仅增加与 t 的长度成比例的量。

  2. Share the tail list t between the old list and the new list, such that the amount of memory in use does not increase—beyond the one extra piece of memory needed to store h + 1.
    在旧列表和新列表之间共享尾列表 t ,这样使用的内存量就不会增加——超出存储 h + 1 所需的一块额外内存。

In fact, the compiler does the latter. So there’s no need for concern. The reason that it’s quite safe for the compiler to implement sharing is exactly that list elements are immutable. If they were instead mutable, then we’d start having to worry about whether the list I have is shared with the list you have, and whether changes I make will be visible in your list. So immutability makes it easier to reason about the code, and makes it safe for the compiler to perform an optimization.
事实上,编译器执行的是后者。所以没必要担心。编译器之所以能够非常安全地实现共享,正是因为列表元素是不可变的。如果它们是可变的,那么我们就开始担心我的列表是否与您的列表共享,以及我所做的更改是否会在您的列表中可见。因此,不变性使得推理代码变得更加容易,并使编译器可以安全地执行优化。

3.1.4. Pattern Matching with Lists
3.1.4. 与列表的模式匹配 ¶

We saw above how to access lists using pattern matching. Let’s look more carefully at this feature.
我们在上面看到了如何使用模式匹配来访问列表。让我们更仔细地看看这个功能。

Syntax.

match e with
| p1 -> e1
| p2 -> e2
| ...
| pn -> en

Each of the clauses pi -> ei is called a branch or a case of the pattern match. The first vertical bar in the entire pattern match is optional.
每个子句 pi -> ei 称为模式匹配的一个分支或一个情况。整个模式匹配中的第一个竖线是可选的。

The p’s here are a new syntactic form called a pattern. For now, a pattern may be:
这里的 p 是一种新的语法形式,称为模式。目前,一种模式可能是:

  • a variable name, e.g. x
    变量名称,例如 x

  • the underscore character _, which is called the wildcard
    下划线字符 _ ,称为通配符

  • the empty list [] 空列表 []

  • p1 :: p2

  • [p1; ...; pn]

No variable name may appear more than once in a pattern. For example, the pattern x :: x is illegal. The wildcard may occur any number of times.
变量名称在模式中不得出现多次。例如,模式 x :: x 是非法的。通配符可以出现任意多次。

As we learn more of data structures available in OCaml, we’ll expand the possibilities for what a pattern may be.
随着我们更多地了解 OCaml 中可用的数据结构,我们将扩展模式的可能性。

Dynamic semantics. 动态语义。

Pattern matching involves two inter-related tasks: determining whether a pattern matches a value, and determining what parts of the value should be associated with which variable names in the pattern. The former task is intuitively about determining whether a pattern and a value have the same shape. The latter task is about determining the variable bindings introduced by the pattern. For example, consider the following code:
模式匹配涉及两个相互关联的任务:确定模式是否与值匹配,以及确定值的哪些部分应与模式中的哪些变量名称相关联。前一个任务直观地是确定模式和值是否具有相同的形状。后一个任务是关于确定模式引入的变量绑定。例如,考虑以下代码:

match 1 :: [] with
| [] -> false
| h :: t -> h >= 1 && List.length t = 0
- : bool = true

When evaluating the right-hand side of the second branch, h is bound to 1 and t is bound to []. Let’s write h->1 to mean the variable binding saying that h has value 1; this is not a piece of OCaml syntax, but rather a notation we use to reason about the language. So the variable bindings produced by the second branch would be h->1, t->[].
当评估第二个分支的右侧时, h 绑定到 1t 绑定到 [] 。让我们写成 h->1 来表示变量绑定说 h 具有值 1 ;这不是 OCaml 语法的一部分,而是我们用来推理该语言的符号。因此第二个分支生成的变量绑定将为 h->1, t->[]

Using that notation, here is a definition of when a pattern matches a value and the bindings that match produces:
使用该表示法,以下是模式何时匹配值以及匹配产生的绑定的定义:

  • The pattern x matches any value v and produces the variable binding x->v.
    模式 x 匹配任何值 v 并生成变量绑定 x->v

  • The pattern _ matches any value and produces no bindings.
    模式 _ 匹配任何值并且不产生绑定。

  • The pattern [] matches the value [] and produces no bindings.
    模式 [] 与值 [] 匹配并且不生成任何绑定。

  • If p1 matches v1 and produces a set b1 of bindings, and if p2 matches v2 and produces a set b2 of bindings, then p1 :: p2 matches v1 :: v2 and produces the set b1b2 of bindings. Note that v2 must be a list (since it’s on the right-hand side of ::) and could have any length: 0 elements, 1 element, or many elements. Note that the union b1b2 of bindings will never have a problem where the same variable is bound separately in both b1 and b2 because of the syntactic restriction that no variable name may appear more than once in a pattern.
    如果 p1 匹配 v1 并生成一组 b1 绑定,并且如果 p2 匹配 v2 并生成绑定集 b2 ,然后 p1 :: p2 匹配 v1 :: v2 并生成绑定集 b1b2 。请注意, v2 必须是一个列表(因为它位于 :: 的右侧)并且可以具有任意长度:0 个元素、1 个元素或多个元素。请注意,绑定的联合 b1b2 永远不会出现在 b1b2 中分别绑定相同变量的问题,因为没有变量的语法限制名称可能在模式中出现多次。

  • If for all i in 1..n, it holds that pi matches vi and produces the set bi of bindings, then [p1; ...; pn] matches [v1; ...; vn] and produces the set ibi of bindings. Note that this pattern specifies the exact length the list must be.
    如果对于 1..n 中的所有 i ,它认为 pi 匹配 vi 并生成绑定集合 bi ,则 [p1; ...; pn] 匹配 [v1; ...; vn] 并生成绑定集 ibi 。请注意,此模式指定列表必须的确切长度。

Now we can say how to evaluate match e with p1 -> e1 | ... | pn -> en:
现在我们可以说如何评估 match e with p1 -> e1 | ... | pn -> en

  • Evaluate e to a value v.
    e 计算为值 v

  • Match v against p1, then against p2, and so on, in the order they appear in the match expression.
    vp1 匹配,然后与 p2 匹配,依此类推,按照它们在匹配表达式中出现的顺序。

  • If v does not match against any of the patterns, then evaluation of the match expression raises a Match_failure exception. We haven’t yet discussed exceptions in OCaml, but you’re surely familiar with them from other languages. We’ll come back to exceptions near the end of this chapter, after we’ve covered some of the other built-in data structures in OCaml.
    如果 v 与任何模式都不匹配,则对匹配表达式的求值会引发 Match_failure 异常。我们尚未讨论 OCaml 中的异常,但您肯定熟悉其他语言中的异常。在我们介绍了 OCaml 中的一些其他内置数据结构之后,我们将在本章末尾回到异常。

  • Otherwise, stop trying to match at the first time a match succeeds against a pattern. Let pi be that pattern and let b be the variable bindings produced by matching v against pi.
    否则,在第一次匹配模式成功时停止尝试匹配。令 pi 为该模式,并令 b 为通过将 vpi 匹配而生成的变量绑定。

  • Substitute those bindings inside ei, producing a new expression e'.
    替换 ei 内的这些绑定,生成新的表达式 e'

  • Evaluate e' to a value v'.
    e' 计算为值 v'

  • The result of the entire match expression is v'.
    整个匹配表达式的结果是 v'

For example, here’s how this match expression would be evaluated:
例如,以下是此匹配表达式的计算方式:

match 1 :: [] with
| [] -> false
| h :: t -> h = 1 && t = []
- : bool = true
  • 1 :: [] is already a value.
    1 :: [] 已经是一个值。

  • [] does not match 1 :: [].
    []1 :: [] 不匹配。

  • h :: t does match 1 :: [] and produces variable bindings {h->1,t->[]}, because:
    h :: t 确实匹配 1 :: [] 并生成变量绑定 { h->1 , t->[] },因为:

    • h matches 1 and produces the variable binding h->1.
      h 匹配 1 并生成变量绑定 h->1

    • t matches [] and produces the variable binding t->[].
      t 匹配 [] 并生成变量绑定 t->[]

  • Substituting {h->1,t->[]} inside h = 1 && t = [] produces a new expression 1 = 1 && [] = [].
    h = 1 && t = [] 中替换 { h->1 , t->[] } 会生成一个新的表达式 1 = 1 && [] = []

  • Evaluating 1 = 1 && [] = [] yields the value true. We omit the justification for that fact here, but it follows from other evaluation rules for built-in operators and function application.
    计算 1 = 1 && [] = [] 会产生值 true 。我们在这里省略了这一事实的理由,但它遵循内置运算符和函数应用程序的其他评估规则。

  • So the result of the entire match expression is true.
    所以整个匹配表达式的结果是 true

Static semantics. 静态语义。

  • If e : ta and for all i, it holds that pi : ta and ei : tb, then (match e with p1 -> e1 | ... | pn -> en) : tb.
    如果 e : ta 且对于所有 i ,它保持 pi : taei : tb ,然后 (match e with p1 -> e1 | ... | pn -> en) : tb

That rule relies on being able to judge whether a pattern has a particular type. As usual, type inference comes into play here. The OCaml compiler infers the types of any pattern variables as well as all occurrences of the wildcard pattern. As for the list patterns, they have the same type-checking rules as list expressions.
该规则依赖于能够判断模式是否具有特定类型。像往常一样,类型推断在这里发挥作用。 OCaml 编译器推断任何模式变量的类型以及所有出现的通配符模式。至于列表模式,它们具有与列表表达式相同的类型检查规则。

Additional Static Checking.
额外的静态检查。

In addition to that type-checking rule, there are two other checks the compiler does for each match expression.
除了类型检查规则之外,编译器还对每个匹配表达式执行两项其他检查。

First, exhaustiveness: the compiler checks to make sure that there are enough patterns to guarantee that at least one of them matches the expression e, no matter what the value of that expression is at run time. This ensures that the programmer did not forget any branches. For example, the function below will cause the compiler to emit a warning:
首先,详尽性:编译器检查以确保有足够的模式来保证至少其中一个与表达式 e 匹配,无论该表达式在运行时的值是什么。这确保程序员不会忘记任何分支。例如,下面的函数将导致编译器发出警告:

let head lst = match lst with h :: _ -> h
File "[12]", line 1, characters 15-41:
1 | let head lst = match lst with h :: _ -> h
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val head : 'a list -> 'a = <fun>

By presenting that warning to the programmer, the compiler is helping the programmer to defend against the possibility of Match_failure exceptions at runtime.
通过向程序员提供该警告,编译器正在帮助程序员防范运行时出现 Match_failure 异常的可能性。

Note 笔记

Sorry about how the output from the cell above gets split into many lines in the HTML. That is currently an open issue with JupyterBook, the framework used to build this book.
对于上面单元格的输出如何在 HTML 中分割成许多行感到抱歉。目前,这是用于构建本书的框架 JupyterBook 的一个悬而未决的问题。

Second, unused branches: the compiler checks to see whether any of the branches could never be matched against because one of the previous branches is guaranteed to succeed. For example, the function below will cause the compiler to emit a warning:
其次,未使用的分支:编译器检查是否有任何分支永远无法匹配,因为前面的分支之一保证会成功。例如,下面的函数将导致编译器发出警告:

let rec sum lst =
  match lst with
  | h :: t -> h + sum t
  | [ h ] -> h
  | [] -> 0
File "[13]", line 4, characters 4-9:
4 |   | [ h ] -> h
        ^^^^^
Warning 11 [redundant-case]: this match case is unused.
val sum : int list -> int = <fun>

The second branch is unused because the first branch will match anything the second branch matches.
第二个分支未使用,因为第一个分支将匹配第二个分支匹配的任何内容。

Unused match cases are usually a sign that the programmer wrote something other than what they intended. So by presenting that warning, the compiler is helping the programmer to detect latent bugs in their code.
未使用的匹配情况通常表明程序员编写了与预期不同的内容。因此,通过显示该警告,编译器正在帮助程序员检测代码中的潜在错误。

Here’s an example of one of the most common bugs that causes an unused match case warning. Understanding it is also a good way to check your understanding of the dynamic semantics of match expressions:
以下是导致未使用的匹配案例警告的最常见错误之一的示例。理解它也是检查您对匹配表达式的动态语义的理解的好方法:

let length_is lst n =
  match List.length lst with
  | n -> true
  | _ -> false
File "[14]", line 4, characters 4-5:
4 |   | _ -> false
        ^
Warning 11 [redundant-case]: this match case is unused.
val length_is : 'a list -> 'b -> bool = <fun>

The programmer was thinking that if the length of lst is equal to n, then this function will return true, and otherwise will return false. But in fact this function always returns true. Why? Because the pattern variable n is distinct from the function argument n. Suppose that the length of lst is 5. Then the pattern match becomes: match 5 with n -> true | _ -> false. Does n match 5? Yes, according to the rules above: a variable pattern matches any value and here produces the binding n->5. Then evaluation applies that binding to true, substituting all occurrences of n inside of true with 5. Well, there are no such occurrences. So we’re done, and the result of evaluation is just true.
程序员认为如果 lst 的长度等于 n ,那么这个函数将返回 true ,否则将返回 false .但事实上这个函数总是返回 true 。为什么?因为模式变量 n 与函数参数 n 不同。假设 lst 的长度为 5,则模式匹配变为: match 5 with n -> true | _ -> falsen 匹配 5 吗?是的,根据上面的规则:变量模式匹配任何值,并在此处生成绑定 n->5 。然后评估将该绑定应用于 true ,用 5 替换 true 内所有出现的 n 。嗯,没有出现这样的情况。这样我们就完成了,评估的结果就是 true

What the programmer really meant to write was:
程序员真正想写的是:

let length_is lst n =
  match List.length lst with
  | m -> m = n
val length_is : 'a list -> int -> bool = <fun>

or better yet: 或者更好:

let length_is lst n =
  List.length lst = n
val length_is : 'a list -> int -> bool = <fun>

3.1.5. Deep Pattern Matching
3.1.5. 深层模式匹配 ¶

Patterns can be nested. Doing so can allow your code to look deeply into the structure of a list. For example:
模式可以嵌套。这样做可以让您的代码深入了解列表的结构。例如:

  • _ :: [] matches all lists with exactly one element
    _ :: [] 匹配所有仅包含一个元素的列表

  • _ :: _ matches all lists with at least one element
    _ :: _ 匹配所有至少包含一个元素的列表

  • _ :: _ :: [] matches all lists with exactly two elements
    _ :: _ :: [] 匹配所有包含两个元素的列表

  • _ :: _ :: _ :: _ matches all lists with at least three elements
    _ :: _ :: _ :: _ 匹配所有至少包含三个元素的列表

3.1.6. Immediate Matches
3.1.6. 直接匹配 ¶

When you have a function that immediately pattern-matches against its final argument, there’s a nice piece of syntactic sugar you can use to avoid writing extra code. Here’s an example: instead of
当您有一个立即与其最终参数进行模式匹配的函数时,您可以使用一个很好的语法糖来避免编写额外的代码。这是一个例子:不是

let rec sum lst =
  match lst with
  | [] -> 0
  | h :: t -> h + sum t
val sum : int list -> int = <fun>

you can write 而是可以写

let rec sum = function
  | [] -> 0
  | h :: t -> h + sum t
val sum : int list -> int = <fun>

The word function is a keyword. Notice that we’re able to leave out the line containing match as well as the name of the argument, which was never used anywhere else but that line. In such cases, though, it’s especially important in the specification comment for the function to document what that argument is supposed to be, since the code no longer gives it a descriptive name.
单词 function 是一个关键字。请注意,我们可以省略包含 match 的行以及参数名称,除了该行之外,该名称从未在其他任何地方使用过。不过,在这种情况下,在函数的规范注释中记录该参数的含义尤其重要,因为代码不再为其提供描述性名称。

3.1.7. OCamldoc and List Syntax
3.1.7. OCamldoc 和列表语法 ¶

OCamldoc is a documentation generator similar to Javadoc. It extracts comments from source code and produces HTML (as well as other output formats). The standard library web documentation for the List module is generated by OCamldoc from the standard library source code for that module, for example.
OCamldoc 是一个类似于 Javadoc 的文档生成器。它从源代码中提取注释并生成 HTML(以及其他输出格式)。例如,List 模块的标准库 Web 文档是由 OCamldoc 根据该模块的标准库源代码生成的。

Warning 警告

There is a syntactic convention with square brackets in OCamldoc that can be confusing with respect to lists.
OCamldoc 中有一个带有方括号的语法约定,对于列表可能会造成混淆。

In an OCamldoc comment, source code is surrounded by square brackets. That code will be rendered in typewriter face and syntax-highlighted in the output HTML. The square brackets in this case do not indicate a list.
在 OCamldoc 注释中,源代码用方括号括起来。该代码将以打字机形式呈现,并在输出 HTML 中突出显示语法。本例中的方括号并不表示列表。

For example, here is the comment for List.hd in the standard library source code:
例如,以下是标准库源代码中 List.hd 的注释:

(** Return the first element of the given list. Raise
   [Failure "hd"] if the list is empty. *)

The [Failure "hd"] does not mean a list containing the exception Failure "hd". Rather it means to typeset the expression Failure "hd" as source code, as you can see here.
[Failure "hd"] 并不意味着包含异常 Failure "hd" 的列表。相反,它意味着将表达式 Failure "hd" 排版为源代码,正如您在此处看到的那样。

This can get especially confusing when you want to talk about lists as part of the documentation. For example, here is a way we could rewrite that comment:
当您想将列表作为文档的一部分进行讨论时,这可能会变得特别令人困惑。例如,我们可以通过以下方式重写该评论:

(** [hd lst] returns the first element of [lst].
    Raises [Failure "hd"] if [lst = []]. *)

In [lst = []], the outer square brackets indicate source code as part of a comment, whereas the inner square brackets indicate the empty list.
[lst = []] 中,外部方括号指示源代码作为注释的一部分,而内部方括号指示空列表。

3.1.8. List Comprehensions
3.1.8. 列表推导式 ¶

Some languages, including Python and Haskell, have a syntax called comprehension that allows lists to be written somewhat like set comprehensions from mathematics. The earliest example of comprehensions seems to be the functional language NPL, which was designed in 1977.
一些语言,包括 Python 和 Haskell,有一种称为推导式的语法,允许列表的编写有点像数学中的集合推导式。推导式最早的例子似乎是函数式语言 NPL,它设计于 1977 年。

OCaml doesn’t have built-in syntactic support for comprehensions. Though some extensions were developed, none seem to be supported any longer. The primary tasks accomplished by comprehensions (filtering out some elements, and transforming others) are actually well-supported already by higher-order programming, which we’ll study in a later chapter, and the pipeline operator, which we’ve already learned. So an additional syntax for comprehensions was never really needed.
OCaml 没有内置的推导式语法支持。尽管开发了一些扩展,但似乎不再支持任何扩展。通过推导式完成的主要任务(过滤掉一些元素,并转换其他元素)实际上已经得到了高阶编程(我们将在后面的章节中研究)和管道运算符(我们已经学习过)的良好支持。因此,从来没有真正需要额外的推导式语法。

3.1.9. Tail Recursion 3.1.9. 尾递归 ¶

Recall that a function is tail recursive if it calls itself recursively but does not perform any computation after the recursive call returns, and immediately returns to its caller the value of its recursive call. Consider these two implementations, sum and sum_tr of summing a list:
回想一下,如果一个函数递归地调用自身,但在递归调用返回后不执行任何计算,并立即将其递归调用的值返回给调用者,则该函数是尾递归的。考虑这两个实现, sumsum_tr 对列表求和:

let rec sum (l : int list) : int =
  match l with
  | [] -> 0
  | x :: xs -> x + (sum xs)

let rec sum_plus_acc (acc : int) (l : int list) : int =
  match l with
  | [] -> acc
  | x :: xs -> sum_plus_acc (acc + x) xs

let sum_tr : int list -> int =
  sum_plus_acc 0
val sum : int list -> int = <fun>
val sum_plus_acc : int -> int list -> int = <fun>
val sum_tr : int list -> int = <fun>

Observe the following difference between the sum and sum_tr functions above: In the sum function, which is not tail recursive, after the recursive call returned its value, we add x to it. In the tail recursive sum_tr, or rather in sum_plus_acc, after the recursive call returns, we immediately return the value without further computation.
观察上面的 sumsum_tr 函数之间的以下区别:在不是尾递归的 sum 函数中,在递归调用返回其值之后,我们添加 x 到它。在尾部递归 sum_tr 中,或者更确切地说在 sum_plus_acc 中,递归调用返回后,我们立即返回值,而无需进一步计算。

If you’re going to write functions on really long lists, tail recursion becomes important for performance. So when you have a choice between using a tail-recursive vs. non-tail-recursive function, you are likely better off using the tail-recursive function on really long lists to achieve space efficiency. For that reason, the List module documents which functions are tail recursive and which are not.
如果您要在很长的列表上编写函数,尾递归对于性能就变得很重要。因此,当您在使用尾递归函数与非尾递归函数之间进行选择时,您可能最好在非常长的列表上使用尾递归函数以实现空间效率。因此,List 模块记录了哪些函数是尾递归的,哪些不是。

But that doesn’t mean that a tail-recursive implementation is strictly better. For example, the tail-recursive function might be harder to read. (Consider sum_plus_acc.) Also, there are cases where implementing a tail-recursive function entails having to do a pre- or post-processing pass to reverse the list. On small to medium sized lists, the overhead of reversing the list (both in time and in allocating memory for the reversed list) can make the tail-recursive version less time efficient. What constitutes “small” vs. “big” here? That’s hard to say, but maybe 10,000 is a good estimate, according to the standard library documentation of the List module.
但这并不意味着尾递归实现绝对更好。例如,尾递归函数可能更难阅读。 (考虑 sum_plus_acc 。)此外,在某些情况下,实现尾递归函数需要执行预处理或后处理过程来反转列表。在中小型列表上,反转列表的开销(时间和为反转列表分配内存)可能会降低尾递归版本的时间效率。这里的“小”与“大”是什么?这很难说,但根据 List 模块的标准库文档,10,000 可能是一个不错的估计。

Here is a useful tail-recursive function to produce a long list:
这是一个有用的尾递归函数,可以生成长列表:

(** [from i j l] is the list containing the integers from [i] to [j],
    inclusive, followed by the list [l].
    Example:  [from 1 3 [0] = [1; 2; 3; 0]] *)
let rec from i j l = if i > j then l else from i (j - 1) (j :: l)

(** [i -- j] is the list containing the integers from [i] to [j], inclusive. *)
let ( -- ) i j = from i j []

let long_list = 0 -- 1_000_000
val from : int -> int -> int list -> int list = <fun>
val ( -- ) : int -> int -> int list = <fun>
val long_list : int list =
  [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20;
   21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38;
   39; 40; 41; 42; 43; 44; 45; 46; 47; 48; 49; 50; 51; 52; 53; 54; 55; 56;
   57; 58; 59; 60; 61; 62; 63; 64; 65; 66; 67; 68; 69; 70; 71; 72; 73; 74;
   75; 76; 77; 78; 79; 80; 81; 82; 83; 84; 85; 86; 87; 88; 89; 90; 91; 92;
   93; 94; 95; 96; 97; 98; 99; 100; 101; 102; 103; 104; 105; 106; 107; 108;
   109; 110; 111; 112; 113; 114; 115; 116; 117; 118; 119; 120; 121; 122; 123;
   124; 125; 126; 127; 128; 129; 130; 131; 132; 133; 134; 135; 136; 137; 138;
   139; 140; 141; 142; 143; 144; 145; 146; 147; 148; 149; 150; 151; 152; 153;
   154; 155; 156; 157; 158; 159; 160; 161; 162; 163; 164; 165; 166; 167; 168;
   169; 170; 171; 172; 173; 174; 175; 176; 177; 178; 179; 180; 181; 182; 183;
   184; 185; 186; 187; 188; 189; 190; 191; 192; 193; 194; 195; 196; 197; 198;
   199; 200; 201; 202; 203; 204; 205; 206; 207; 208; 209; 210; 211; 212; 213;
   214; 215; 216; 217; 218; 219; 220; 221; 222; 223; 224; 225; 226; 227; 228;
   229; 230; 231; 232; 233; 234; 235; 236; 237; 238; 239; 240; 241; 242; 243;
   244; 245; 246; 247; 248; 249; 250; 251; 252; 253; 254; 255; 256; 257; 258;
   259; 260; 261; 262; 263; 264; 265; 266; 267; 268; 269; 270; 271; 272; 273;
   274; 275; 276; 277; 278; 279; 280; 281; 282; 283; 284; 285; 286; 287; 288;
   289; 290; 291; 292; 293; 294; 295; 296; 297; 298; ...]

It would be worthwhile to study the definition of -- to convince yourself that you understand (i) how it works and (ii) why it is tail recursive.
研究 -- 的定义是值得的,以说服自己理解(i)它是如何工作的以及(ii)为什么它是尾递归的。

You might in the future decide you want to create such a list again. Rather than having to remember where this definition is, and having to copy it into your code, here’s an easy way to create the same list using a built-in library function:
您将来可能会决定再次创建这样的列表。不必记住这个定义的位置,也不必将其复制到代码中,而是使用内置库函数创建相同列表的简单方法:

List.init 1_000_000 Fun.id
- : int list =
[0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20;
 21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38; 39;
 40; 41; 42; 43; 44; 45; 46; 47; 48; 49; 50; 51; 52; 53; 54; 55; 56; 57; 58;
 59; 60; 61; 62; 63; 64; 65; 66; 67; 68; 69; 70; 71; 72; 73; 74; 75; 76; 77;
 78; 79; 80; 81; 82; 83; 84; 85; 86; 87; 88; 89; 90; 91; 92; 93; 94; 95; 96;
 97; 98; 99; 100; 101; 102; 103; 104; 105; 106; 107; 108; 109; 110; 111; 112;
 113; 114; 115; 116; 117; 118; 119; 120; 121; 122; 123; 124; 125; 126; 127;
 128; 129; 130; 131; 132; 133; 134; 135; 136; 137; 138; 139; 140; 141; 142;
 143; 144; 145; 146; 147; 148; 149; 150; 151; 152; 153; 154; 155; 156; 157;
 158; 159; 160; 161; 162; 163; 164; 165; 166; 167; 168; 169; 170; 171; 172;
 173; 174; 175; 176; 177; 178; 179; 180; 181; 182; 183; 184; 185; 186; 187;
 188; 189; 190; 191; 192; 193; 194; 195; 196; 197; 198; 199; 200; 201; 202;
 203; 204; 205; 206; 207; 208; 209; 210; 211; 212; 213; 214; 215; 216; 217;
 218; 219; 220; 221; 222; 223; 224; 225; 226; 227; 228; 229; 230; 231; 232;
 233; 234; 235; 236; 237; 238; 239; 240; 241; 242; 243; 244; 245; 246; 247;
 248; 249; 250; 251; 252; 253; 254; 255; 256; 257; 258; 259; 260; 261; 262;
 263; 264; 265; 266; 267; 268; 269; 270; 271; 272; 273; 274; 275; 276; 277;
 278; 279; 280; 281; 282; 283; 284; 285; 286; 287; 288; 289; 290; 291; 292;
 293; 294; 295; 296; 297; 298; ...]

Expression List.init len f creates the list [f 0; f 1; ...; f (len - 1)], and it does so tail recursively if len is bigger than 10,000. Function Fun.id is simply the identify function fun x -> x.
表达式 List.init len f 创建列表 [f 0; f 1; ...; f (len - 1)] ,如果 len 大于 10,000,则它会递归地执行尾部操作。函数 Fun.id 只是识别函数 fun x -> x

3.2. Variants 3.2. 不定型 ¶

A variant is a data type representing a value that is one of several possibilities. At their simplest, variants are like enums from C or Java:
不定型(变体)是一种数据类型,表示一个值,该值是多种可能性之一。最简单的说,变体就像 C 或 Java 中的枚举:

type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
let d = Tue
type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
val d : day = Tue

The individual names of the values of a variant are called constructors in OCaml. In the example above, the constructors are Sun, Mon, etc. This is a somewhat different use of the word constructor than in C++ or Java.
变体值的各个名称在 OCaml 中称为构造函数。在上面的示例中,构造函数为 SunMon 等。构造函数一词的用法与 C++ 或 Java 中有些不同。

For each kind of data type in OCaml, we’ve been discussing how to build and access it. For variants, building is easy: just write the name of the constructor. For accessing, we use pattern matching. For example:
对于 OCaml 中的每种数据类型,我们一直在讨论如何构建和访问它。对于变体,构建很简单:只需写下构造函数的名称即可。为了访问,我们使用模式匹配。例如:

let int_of_day d =
  match d with
  | Sun -> 1
  | Mon -> 2
  | Tue -> 3
  | Wed -> 4
  | Thu -> 5
  | Fri -> 6
  | Sat -> 7
val int_of_day : day -> int = <fun>

There isn’t any kind of automatic way of mapping a constructor name to an int, like you might expect from languages with enums.
没有任何类型的自动方法将构造函数名称映射到 int ,就像您对具有枚举的语言所期望的那样。

Syntax.

Defining a variant type:
定义变体类型:

type t = C1 | ... | Cn

The constructor names must begin with an uppercase letter. OCaml uses that to distinguish constructors from variable identifiers.
构造函数名称必须以大写字母开头。 OCaml 使用它来区分构造函数和变量标识符。

The syntax for writing a constructor value is simply its name, e.g., C.
编写构造函数值的语法就是它的名称,例如 C

Dynamic semantics. 动态语义。

  • A constructor is already a value. There is no computation to perform.
    构造函数已经是一个值。无需执行任何计算。

Static semantics. 静态语义。

  • If t is a type defined as type t = ... | C | ..., then C : t.
    如果 t 是定义为 type t = ... | C | ... 的类型,则 C : t

3.2.1. Scope 3.2.1. 范围类型 ¶

Suppose there are two types defined with overlapping constructor names, for example,
假设有两个类型定义了重叠的构造函数名称,例如,

type t1 = C | D
type t2 = D | E
let x = D
type t1 = C | D
type t2 = D | E
val x : t2 = D

When D appears after these definitions, to which type does it refer? That is, what is the type of x above? The answer is that the type defined later wins. So x : t2. That is potentially surprising to programmers, so within any given scope (e.g., a file or a module, though we haven’t covered modules yet) it’s idiomatic whenever overlapping constructor names might occur to prefix them with some distinguishing character. For example, suppose we’re defining types to represent Pokémon:
D 出现在这些定义之后时,它指的是哪种类型?即上面的 x 是什么类型?答案是后面定义的类型获胜。所以 x : t2 。这可能会让程序员感到惊讶,因此在任何给定范围内(例如,文件或模块,尽管我们还没有讨论模块),每当可能出现重叠的构造函数名称时,都会惯用一些区分字符作为前缀。例如,假设我们定义类型来表示 Pokémon:

type ptype =
  TNormal | TFire | TWater

type peff =
  ENormal | ENotVery | ESuper
type ptype = TNormal | TFire | TWater
type peff = ENormal | ENotVery | ESuper

Because “Normal” would naturally be a constructor name for both the type of a Pokémon and the effectiveness of a Pokémon attack, we add an extra character in front of each constructor name to indicate whether it’s a type or an effectiveness.
因为“Normal”自然是神奇宝贝类型和神奇宝贝攻击有效性的构造函数名称,所以我们在每个构造函数名称前面添加一个额外的字符来指示它是类型还是有效性。

3.2.2. Pattern Matching
3.2.2. 模式匹配 ¶

Each time we introduced a new kind of data type, we need to introduce the new patterns associated with it. For variants, this is easy. We add the following new pattern form to the list of legal patterns:
每次我们引入一种新的数据类型时,我们都需要引入与之相关的新模式。对于变体来说,这很容易。我们将以下新模式形式添加到合法模式列表中:

  • a constructor name C
    构造函数名称 C

And we extend the definition of when a pattern matches a value and produces a binding as follows:
我们扩展了模式何时匹配值并生成绑定的定义,如下所示:

  • The pattern C matches the value C and produces no bindings.
    模式 C 与值 C 匹配并且不生成任何绑定。

Note 笔记

Variants are considerably more powerful than what we have seen here. We’ll return to them again soon.
变体比我们在这里看到的要强大得多。我们很快就会再次联系他们。

3.3. Unit Testing with OUnit
3.3. 用 OUnit 单元测试 ¶

Note 笔记

This section is a bit of a detour from our study of data types, but it’s a good place to take the detour: we now know just enough to understand how unit testing can be done in OCaml, and there’s no good reason to wait any longer to learn about it.
本节与我们对数据类型的研究有些绕道,但这是绕道的好地方:我们现在知道的知识足以理解如何在 OCaml 中完成单元测试,并且没有理由再等待了了解它。

Using the toplevel to test functions will only work for very small programs. Larger programs need test suites that contain many unit tests and can be re-run every time we update our code base. A unit test is a test of one small piece of functionality in a program, such as an individual function.
使用顶层来测试函数只适用于非常小的程序。较大的程序需要包含许多单元测试的测试套件,并且每次更新代码库时都可以重新运行。单元测试是对程序中一小部分功能(例如单个函数)的测试。

We’ve now learned enough features of OCaml to see how to do unit testing with a library called OUnit. It is a unit testing framework similar to JUnit in Java, HUnit in Haskell, etc. The basic workflow for using OUnit is as follows:
现在我们已经了解了 OCaml 的足够功能,可以了解如何使用名为 OUnit 的库进行单元测试。它是一个类似于Java中的JUnit、Haskell中的HUnit等的单元测试框架。使用OUnit的基本工作流程如下:

  • Write a function in a file f.ml. There could be many other functions in that file too.
    在文件 f.ml 中编写函数。该文件中还可能有许多其他函数。

  • Write unit tests for that function in a separate file test.ml. That exact name is not actually essential.
    在单独的文件 test.ml 中编写该函数的单元测试。这个确切的名称实际上并不重要。

  • Build and run test to execute the unit tests.
    构建并运行 test 以执行单元测试。

The OUnit documentation is available on GitHub.
OUnit 文档可在 GitHub 上获取。

3.3.1. An Example of OUnit
3.3.1. OUnit 的一个例子 ¶

The following example shows you how to create an OUnit test suite. There are some things in the example that might at first seem mysterious; they are discussed in the next section.
以下示例向您展示如何创建 OUnit 测试套件。这个例子中的一些东西乍一看可能看起来很神秘;它们将在下一节中讨论。

Create a new directory. In that directory, create a file named sum.ml, and put the following code into it:
创建一个新目录。在该目录中,创建一个名为 sum.ml 的文件,并将以下代码放入其中:

let rec sum = function
  | [] -> 0
  | x :: xs -> x + sum xs

Now create a second file named test.ml, and put this code into it:
现在创建第二个名为 test.ml 的文件,并将以下代码放入其中:

open OUnit2
open Sum

let tests = "test suite for sum" >::: [
  "empty" >:: (fun _ -> assert_equal 0 (sum []));
  "singleton" >:: (fun _ -> assert_equal 1 (sum [1]));
  "two_elements" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

let _ = run_test_tt_main tests

Depending on your editor and its configuration, you probably now see some “Unbound module” errors about OUnit2 and Sum. Don’t worry; the code is actually correct. We just need to set up dune and tell it to link OUnit. Create a dune file and put this in it:
根据您的编辑器及其配置,您现在可能会看到一些有关 OUnit2 和 Sum 的“未绑定模块”错误。不用担心;该代码实际上是正确的。我们只需要设置dune 并告诉它链接OUnit。创建一个 dune 文件并将其放入其中:

(executable
 (name test)
 (libraries ounit2))

And create a dune-project file as usual:
并像往常一样创建一个 dune-project 文件:

(lang dune 3.4)

Now build the test suite:
现在构建测试套件:

$ dune build test.exe

Go back to your editor and do anything that will cause it to revisit test.ml. You can close and re-open the window, or make a trivial change in the file (e.g., add then delete a space). Now the errors should all disappear.
返回编辑器并执行任何会导致其重新访问 test.ml 的操作。您可以关闭并重新打开窗口,或者在文件中进行一些细微的更改(例如,添加然后删除空格)。现在错误应该全部消失。

Finally, you can run the test suite:
最后,您可以运行测试套件:

$ dune exec ./test.exe

You will get a response something like this:
您将得到如下响应:

...
Ran: 3 tests in: 0.12 seconds.
OK

Now suppose we modify sum.ml to introduce a bug by changing the code in it to the following:
现在假设我们修改 sum.ml 通过将其中的代码更改为以下内容来引入错误:

let rec sum = function
  | [] -> 1 (* bug *)
  | x :: xs -> x + sum xs

If rebuild and re-execute the test suite, all test cases now fail. The output tells us the names of the failing cases. Here’s the beginning of the output, in which we’ve replaced some strings that will be dependent on your own local computer with ...:
如果重建并重新执行测试套件,所有测试用例现在都会失败。输出告诉我们失败案例的名称。这是输出的开头,其中我们用 ... 替换了一些依赖于您自己的本地计算机的字符串:

FFF
==============================================================================
Error: test suite for sum:2:two_elements.

File ".../_build/oUnit-test suite for sum-...#01.log", line 9, characters 1-1:
Error: test suite for sum:2:two_elements (in the log).

Raised at OUnitAssert.assert_failure in file "src/lib/ounit2/advanced/oUnitAssert.ml", line 45, characters 2-27
Called from OUnitRunner.run_one_test.(fun) in file "src/lib/ounit2/advanced/oUnitRunner.ml", line 83, characters 13-26

not equal
------------------------------------------------------------------------------

The first line of that output
该输出的第一行

FFF

tells us that OUnit ran three test cases and all three failed.
告诉我们 OUnit 运行了三个测试用例,但所有三个测试用例都失败了。

The next interesting line
下一个有趣的行

Error: test suite for sum:2:two_elements.

tells us that in the test suite named test suite for sum the test case at index 2 named two_elements failed. The rest of the output for that test case is not particularly interesting; let’s ignore it for now.
告诉我们,在名为 test suite for sum 的测试套件中,索引 2 处名为 two_elements 的测试用例失败。该测试用例的其余输出并不是特别有趣;我们暂时忽略它。

3.3.2. Explanation of the OUnit Example
3.3.2. OUnit 示例说明 ¶

Let’s study more carefully what we just did in the previous section. In the test file, open OUnit2 brings into scope the many definitions in OUnit2, which is version 2 of the OUnit framework. And open Sum brings into scope the definitions from sum.ml. We’ll learn more about scope and the open keyword later in a later chapter.
让我们更仔细地研究一下我们在上一节中所做的事情。在测试文件中, open OUnit2 将 OUnit2 中的许多定义纳入范围,OUnit2 是 OUnit 框架的版本 2。 open Sumsum.ml 的定义纳入范围。我们将在后面的章节中详细了解范围和 open 关键字。

Then we created a list of test cases:
然后我们创建了一个测试用例列表:

[
  "empty"  >:: (fun _ -> assert_equal 0 (sum []));
  "one"    >:: (fun _ -> assert_equal 1 (sum [1]));
  "onetwo" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

Each line of code is a separate test case. A test case has a string giving it a descriptive name, and a function to run as the test case. In between the name and the function we write >::, which is a custom operator defined by the OUnit framework. Let’s look at the first function from above:
每行代码都是一个单独的测试用例。测试用例有一个给出描述性名称的字符串,以及一个作为测试用例运行的函数。在名称和函数之间我们编写 >:: ,这是 OUnit 框架定义的自定义运算符。让我们看一下上面的第一个函数:

fun _ -> assert_equal 0 (sum [])

Every test case function receives as input a parameter that OUnit calls a test context. Here (and in many of the test cases we write) we don’t actually need to worry about the context, so we use the underscore to indicate that the function ignores its input. The function then calls assert_equal, which is a function provided by OUnit that checks to see whether its two arguments are equal. If so the test case succeeds. If not, the test case fails.
每个测试用例函数都接收 OUnit 调用测试上下文的参数作为输入。在这里(以及我们编写的许多测试用例中)我们实际上不需要担心上下文,因此我们使用下划线来指示函数忽略其输入。然后该函数调用 assert_equal ,这是 OUnit 提供的一个函数,用于检查其两个参数是否相等。如果是这样,则测试用例成功。如果不是,则测试用例失败。

Then we created a test suite:
然后我们创建了一个测试套件:

let tests = "test suite for sum" >::: [
  "empty" >:: (fun _ -> assert_equal 0 (sum []));
  "singleton" >:: (fun _ -> assert_equal 1 (sum [1]));
  "two_elements" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

The >::: operator is another custom OUnit operator. It goes between the name of the test suite and the list of test cases in that suite.
>::: 运算符是另一个自定义 OUnit 运算符。它位于测试套件的名称和该套件中的测试用例列表之间。

Then we ran the test suite:
然后我们运行测试套件:

let _ = run_test_tt_main tests

The function run_test_tt_main is provided by OUnit. It runs a test suite and prints the results of which test cases passed vs. which failed to standard output. The use of let _ = here indicates that we don’t care what value the function returns; it just gets discarded.
函数 run_test_tt_main 由OUnit提供。它运行一个测试套件,并将哪些测试用例通过、哪些测试用例失败的结果打印到标准输出。这里使用 let _ = 表明我们不关心函数返回什么值;它只是被丢弃。

3.3.3. Improving OUnit Output
3.3.3. 改进 OUnit 输出 ¶

In our example with the buggy implementation of sum, we got the following output:
在我们的 sum 实现有缺陷的示例中,我们得到以下输出:

==============================================================================
Error: test suite for sum:2:two_elements.
...
not equal
------------------------------------------------------------------------------

The not equal in the OUnit output means that assert_equal discovered the two values passed to it in that test case were not equal. That’s not so informative: we’d like to know why they’re not equal. In particular, we’d like to know what the actual output produced by sum was for that test case. To find out, we need to pass an additional argument to assert_equal. That argument, whose label is printer, should be a function that can transform the outputs to strings. In this case, the outputs are integers, so string_of_int from the Stdlib module will suffice. We modify the test suite as follows:
OUnit 输出中的 not equal 意味着 assert_equal 发现在该测试用例中传递给它的两个值不相等。这并没有提供太多信息:我们想知道为什么它们不相等。特别是,我们想知道 sum 对于该测试用例产生的实际输出是什么。为了找到答案,我们需要向 assert_equal 传递一个附加参数。该参数的标签为 printer ,应该是一个可以将输出转换为字符串的函数。在这种情况下,输出是整数,因此 Stdlib 模块中的 string_of_int 就足够了。我们将测试套件修改如下:

let tests = "test suite for sum" >::: [
  "empty" >:: (fun _ -> assert_equal 0 (sum []) ~printer:string_of_int);
  "singleton" >:: (fun _ -> assert_equal 1 (sum [1]) ~printer:string_of_int);
  "two_elements" >:: (fun _ -> assert_equal 3 (sum [1; 2]) ~printer:string_of_int);
]

And now we get more informative output:
现在我们得到了更多信息输出:

==============================================================================
Error: test suite for sum:2:two_elements.
...
expected: 3 but got: 4
------------------------------------------------------------------------------

That output means that the test named two_elements asserted the equality of 3 and 4. The expected output was 3 because that was the first input to assert_equal, and that function’s specification says that in assert_equal x y, the output you (as the tester) are expecting to get should be x, and the output the function being tested actually produces should be y.
该输出意味着名为 two_elements 的测试断言 34 相等。预期输出是 3 因为这是 assert_equal 的第一个输入,并且该函数的规范表明在 assert_equal x y 中,您(作为测试人员)的输出是期望得到的应该是 x ,被测试的函数实际产生的输出应该是 y

Notice how our test suite is accumulating a lot of redundant code. In particular, we had to add the printer argument to several lines. Let’s improve that code by factoring out a function that constructs test cases:
请注意我们的测试套件如何积累大量冗余代码。特别是,我们必须将 printer 参数添加到多行中。让我们通过分解构建测试用例的函数来改进该代码:

let make_sum_test name expected_output input =
  name >:: (fun _ -> assert_equal expected_output (sum input) ~printer:string_of_int)

let tests = "test suite for sum" >::: [
  make_sum_test "empty" 0 [];
  make_sum_test "singleton" 1 [1];
  make_sum_test "two_elements" 3 [1; 2];
]

For output types that are more complicated than integers, you will end up needing to write your own functions to pass to printer. This is similar to writing toString() methods in Java: for complicated types you invent yourself, the language doesn’t know how to render them as strings. You have to provide the code that does it.
对于比整数更复杂的输出类型,您最终需要编写自己的函数来传递给 printer 。这类似于在 Java 中编写 toString() 方法:对于您自己发明的复杂类型,该语言不知道如何将它们呈现为字符串。您必须提供执行此操作的代码。

3.3.4. Testing for Exceptions
3.3.4. 对异常的测试 ¶

We have a little more of OCaml to learn before we can see how to test for exceptions. You can peek ahead to the section on exceptions if you want to know now.
在了解如何测试异常之前,我们还需要学习更多 OCaml 知识。如果您现在想了解,可以先查看有关例外情况的部分。

3.3.5. Test-Driven Development
3.3.5. 测试驱动的开发 ¶

Testing doesn’t have to happen strictly after you write code. In test-driven development (TDD), testing comes first! It emphasizes incremental development of code: there is always something that can be tested. Testing is not something that happens after implementation; instead, continuous testing is used to catch errors early. Thus, it is important to develop unit tests immediately when the code is written. Automating test suites is crucial so that continuous testing requires essentially no effort.
测试不一定要在编写代码之后严格进行。在测试驱动开发(TDD)中,测试是第一位的!它强调代码的增量开发:总有一些东西可以测试。测试不是在实施之后发生的事情;而是在实施之后发生的事情。相反,持续测试用于尽早发现错误。因此,在编写代码时立即开发单元测试非常重要。自动化测试套件至关重要,因此连续测试基本上不需要任何努力。

Here’s an example of TDD. We deliberately choose an exceedingly simple function to implement, so that the process is clear. Suppose we are working with a datatype for days:
这是 TDD 的一个例子。我们特意选择一个极其简单的功能来实现,这样流程就清晰了。假设我们使用一种数据类型好几天了:

type day = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday

And we want to write a function next_weekday : day -> day that returns the next weekday after a given day. We start by writing the most basic, broken version of that function we can:
我们想要编写一个函数 next_weekday : day -> day 来返回给定日期之后的下一个工作日。我们首先编写该函数的最基本的、损坏的版本,我们可以:

let next_weekday d = failwith "Unimplemented"

Note 笔记

The built-in function failwith raises an exception along with the error message passed to the function.
内置函数 failwith 引发异常以及传递给该函数的错误消息。

Then we write the simplest unit test we can imagine. For example, we know that the next weekday after Monday is Tuesday. So we add a test:
然后我们编写我们能想象到的最简单的单元测试。例如,我们知道星期一之后的下一个工作日是星期二。所以我们添加一个测试:

let tests = "test suite for next_weekday" >::: [
  "tue_after_mon"  >:: (fun _ -> assert_equal Tuesday (next_weekday Monday));
]

Then we run the OUnit test suite. It fails, as expected. That’s good! Now we have a concrete goal, to make that unit test pass. We revise next_weekday to make that happen:
然后我们运行 OUnit 测试套件。正如预期的那样,它失败了。那挺好的!现在我们有一个具体的目标,让单元测试通过。我们修改 next_weekday 来实现这一点:

let next_weekday d =
  match d with
  | Monday -> Tuesday
  | _ -> failwith "Unimplemented"

We compile and run the test; it passes. Time to add some more tests. The simplest remaining possibilities are tests involving just weekdays, rather than weekends. So let’s add tests for weekdays.
我们编译并运行测试;它过去了。是时候添加更多测试了。剩下的最简单的可能性是仅在工作日而不是周末进行测试。因此,让我们添加工作日的测试。

let tests = "test suite for next_weekday" >::: [
  "tue_after_mon"  >:: (fun _ -> assert_equal Tuesday (next_weekday Monday));
  "wed_after_tue"  >:: (fun _ -> assert_equal Wednesday (next_weekday Tuesday));
  "thu_after_wed"  >:: (fun _ -> assert_equal Thursday(next_weekday Wednesday));
  "fri_after_thu"  >:: (fun _ -> assert_equal Friday (next_weekday Thursday));
]

We compile and run the tests; many fail. That’s good! We add new functionality:
我们编译并运行测试;许多人失败了。那挺好的!我们添加新功能:

  let next_weekday d =
    match d with
    | Monday -> Tuesday
    | Tuesday -> Wednesday
    | Wednesday -> Thursday
    | Thursday -> Friday
    | _ -> failwith "Unimplemented"

We compile and run the tests; they pass. At this point we could move on to handling weekends, but we should first notice something about the tests we’ve written: they involve repeating a lot of code. In fact, we probably wrote them by copying-and-pasting the first test, then modifying it for the next three. That’s a sign that we should refactor the code. (As we did before with the sum function we were testing.)
我们编译并运行测试;他们通过了。此时,我们可以继续处理周末,但我们应该首先注意到我们编写的测试的一些内容:它们涉及重复大量代码。事实上,我们可能通过复制并粘贴第一个测试来编写它们,然后针对接下来的三个测试对其进行修改。这表明我们应该重构代码。 (就像我们之前测试的 sum 函数一样。)

Let’s abstract a function that creates test cases for next_weekday:
让我们抽象一个为 next_weekday 创建测试用例的函数:

let make_next_weekday_test name expected_output input=
  name >:: (fun _ -> assert_equal expected_output (next_weekday input))

let tests = "test suite for next_weekday" >::: [
  make_next_weekday_test "tue_after_mon" Tuesday Monday;
  make_next_weekday_test "wed_after_tue" Wednesday Tuesday;
  make_next_weekday_test "thu_after_wed" Thursday Wednesday;
  make_next_weekday_test "fri_after_thu" Friday Thursday;
]

Now we finish the testing and implementation by handling weekends. First we add some test cases:
现在我们通过处理周末来完成测试和实现。首先我们添加一些测试用例:

  ...
  make_next_weekday_test "mon_after_fri" Monday Friday;
  make_next_weekday_test "mon_after_sat" Monday Saturday;
  make_next_weekday_test "mon_after_sun" Monday Sunday;
  ...

Then we finish the function:
然后我们完成这个函数:

let next_weekday d =
  match d with
  | Monday -> Tuesday
  | Tuesday -> Wednesday
  | Wednesday -> Thursday
  | Thursday -> Friday
  | Friday -> Monday
  | Saturday -> Monday
  | Sunday -> Monday

Of course, most people could write that function without errors even if they didn’t use TDD. But rarely do we implement functions that are so simple.
当然,即使不使用 TDD,大多数人也可以毫无错误地编写该函数。但我们很少实现如此简单的功能。

Process. Let’s review the process of TDD:
过程。我们回顾一下TDD的流程:

  • Write a failing unit test case. Run the test suite to prove that the test case fails.
    编写失败的单元测试用例。运行测试套件以证明测试用例失败。

  • Implement just enough functionality to make the test case pass. Run the test suite to prove that the test case passes.
    实现足够的功能以使测试用例通过。运行测试套件以证明测试用例通过。

  • Improve code as needed. In the example above we refactored the test suite, but often we’ll need to refactor the functionality being implemented.
    根据需要改进代码。在上面的示例中,我们重构了测试套件,但通常我们需要重构正在实现的功能。

  • Repeat until you are satisfied that the test suite provides evidence that your implementation is correct.
    重复直到您满意测试套件提供的证据表明您的实施是正确的。

3.4. Records and Tuples
3.4. 记录和元组 ¶

Singly-linked lists are a great data structure, but what if you want a fixed number of elements, instead of an unbounded number? Or what if you want the elements to have distinct types? Or what if you want to access the elements by name instead of by number? Lists don’t make any of those possibilities easy. Instead, OCaml programmers use records and tuples.
单链表是一种很棒的数据结构,但是如果您想要固定数量的元素而不是无限数量的元素怎么办?或者如果您希望元素具有不同的类型怎么办?或者如果您想通过名称而不是数字访问元素怎么办?列表并不能让这些可能性变得容易。相反,OCaml 程序员使用记录和元组。

3.4.1. Records 3.4.1. 记录 ¶

A record is a composite of other types of data, each of which is named. OCaml records are much like structs in C. Here’s an example of a record type definition mon for a Pokémon, re-using the ptype definition from the variants section:
记录是其他类型数据的组合,每种数据都有命名。 OCaml 记录非常类似于 C 中的结构。下面是 Pokémon 的记录类型定义 mon 的示例,重复使用变体部分中的 ptype 定义:

type ptype = TNormal | TFire | TWater
type mon = {name : string; hp : int; ptype : ptype}
type ptype = TNormal | TFire | TWater
type mon = { name : string; hp : int; ptype : ptype; }

This type defines a record with three fields named name, hp (hit points), and ptype. The type of each of those fields is also given. Note that ptype can be used as both a type name and a field name; the namespace for those is distinct in OCaml.
此类型定义了一个包含三个字段的记录,分别名为 namehp (命中点)和 ptype 。还给出了每个字段的类型。请注意, ptype 既可以用作类型名称,也可以用作字段名称;它们的命名空间在 OCaml 中是不同的。

To build a value of a record type, we write a record expression, which looks like this:
为了构建记录类型的值,我们编写一个记录表达式,如下所示:

{name = "Charmander"; hp = 39; ptype = TFire}
- : mon = {name = "Charmander"; hp = 39; ptype = TFire}

So in a type definition we write a colon between the name and the type of a field, but in an expression we write an equals sign.
因此,在类型定义中,我们在字段的名称和类型之间写一个冒号,但在表达式中,我们写一个等号。

To access a record and get a field from it, we use the dot notation that you would expect from many other languages. For example:
为了访问记录并从中获取字段,我们使用您在许多其他语言中所期望的点表示法。例如:

let c = {name = "Charmander"; hp = 39; ptype = TFire};;
c.hp
val c : mon = {name = "Charmander"; hp = 39; ptype = TFire}
- : int = 39

It’s also possible to use pattern matching to access record fields:
还可以使用模式匹配来访问记录字段:

match c with {name = n; hp = h; ptype = t} -> h
- : int = 39

The n, h, and t here are pattern variables. There is a syntactic sugar provided if you want to use the same name for both the field and a pattern variable:
这里的 nht 是模式变量。如果您想对字段和模式变量使用相同的名称,可以使用语法糖:

match c with {name; hp; ptype} -> hp
- : int = 39

Here, the pattern {name; hp; ptype} is sugar for {name = name; hp = hp; ptype = ptype}. In each of those subexpressions, the identifier appearing on the left-hand side of the equals is a field name, and the identifier appearing on the right-hand side is a pattern variable.
这里,模式 {name; hp; ptype}{name = name; hp = hp; ptype = ptype} 的糖。在每个子表达式中,出现在 equals 左侧的标识符是字段名称,出现在右侧的标识符是模式变量。

Syntax.

A record expression is written:
记录表达式写为:

{f1 = e1; ...; fn = en}

The order of the fi=ei inside a record expression is irrelevant. For example, {f = e1; g = e2} is entirely equivalent to {g = e2; f = e1}.
记录表达式内 fi=ei 的顺序无关紧要。例如, {f = e1; g = e2} 完全等同于 {g = e2; f = e1}

A field access is written:
字段访问的写法是:

e.f

where f must be an identifier of a field name, not an expression. That restriction is the same as in any other language with similar features——for example, Java field names. If you really do want to compute which identifier to access, then actually you want a different data structure: a map (also known by many other names: a dictionary or association list or hash table etc., though there are subtle differences implied by each of those terms.)
其中 f 必须是字段名称的标识符,而不是表达式。该限制与具有类似功能的任何其他语言相同——例如 Java 字段名称。如果您确实想计算要访问哪个标识符,那么实际上您需要一个不同的数据结构:映射(也有许多其他名称:字典或关联列表或哈希表等,尽管每个数据结构都隐含着细微的差异)这些术语。)

Dynamic semantics. 动态语义。

  • If for all i in 1..n, it holds that ei ==> vi, then {f1 = e1; ...; fn = en} ==> {f1 = v1; ...; fn = vn}.
    如果对于 1..n 中的所有 i ,它保持 ei ==> vi ,然后 {f1 = e1; ...; fn = en} ==> {f1 = v1; ...; fn = vn}

  • If e ==> {...; f = v; ...} then e.f ==> v.
    如果 e ==> {...; f = v; ...}e.f ==> v

Static semantics. 静态语义。

A record type is written:
记录类型的写法为:

{f1 : t1; ...; fn : tn}

The order of the fi:ti inside a record type is irrelevant. For example, {f : t1; g : t2} is entirely equivalent to {g:t2;f:t1}.
记录类型内 fi:ti 的顺序无关紧要。例如, {f : t1; g : t2} 完全等同于 {g:t2;f:t1}

Note that record types must be defined before they can be used. This enables OCaml to do better type inference than would be possible if record types could be used without definition.
请注意,必须先定义记录类型,然后才能使用它们。与无需定义即可使用记录类型相比,这使得 OCaml 能够进行更好的类型推断。

The type checking rules are:
类型检查规则是:

  • If for all i in 1..n, it holds that ei : ti, and if t is defined to be {f1 : t1; ...; fn : tn}, then {f1 = e1; ...; fn = en} : t. Note that the set of fields provided in a record expression must be the full set of fields defined as part of the record’s type (but see below regarding record copy).
    如果对于 1..n 中的所有 i ,它保持 ei : ti ,并且如果 t 定义为 {f1 : t1; ...; fn : tn} ,然后 {f1 = e1; ...; fn = en} : t 。请注意,记录表达式中提供的字段集必须是定义为记录类型一部分的完整字段集(但请参阅下面有关记录复制的内容)。

  • If e : t1 and if t1 is defined to be {...; f : t2; ...}, then e.f : t2.
    如果 e : t1t1 定义为 {...; f : t2; ...} ,则 e.f : t2

Record copy. 记录副本。

Another syntax is also provided to construct a new record out of an old record:
还提供了另一种语法来从旧记录构造新记录:

{e with f1 = e1; ...; fn = en}

This doesn’t mutate the old record. Rather, it constructs a new record with new values. The set of fields provided after the with does not have to be the full set of fields
不必是完整的字段集
defined as part of the record’s type. In the newly-copied record, any field not provided as part of the with is copied from the old record.
这不会改变旧记录。相反,它用新值构建新记录。 with 之后提供的字段集不必是定义为记录类型一部分的完整字段集。在新复制的记录中,任何未作为 with 的一部分提供的字段都是从旧记录中复制的。

Record copy is syntactic sugar. It’s equivalent to writing
记录副本是语法糖。相当于写

{ f1 = e1;   ...; fn = en;
  g1 = e.g1; ...; gn = e.gn }

where the set of gi is the set of all fields of the record’s type minus the set of fi.
其中 gi 的集合是记录类型的所有字段的集合减去 fi 的集合。

Pattern matching. 模式匹配。

We add the following new pattern form to the list of legal patterns:
我们将以下新模式形式添加到合法模式列表中:

  • {f1 = p1; ...; fn = pn}

And we extend the definition of when a pattern matches a value and produces a binding as follows:
我们扩展了模式何时匹配值并生成绑定的定义,如下所示:

  • If for all i in 1..n, it holds that pi matches vi and produces bindings bi, then the record pattern {f1 = p1; ...; fn = pn} matches the record value {f1 = v1; ...; fn = vn; ...} and produces the set ibi of bindings. Note that the record value may have more fields than the record pattern does.
    如果对于 1..n 中的所有 i ,它认为 pi 匹配 vi 并生成绑定 bi ,那么记录模式 {f1 = p1; ...; fn = pn} 与记录值 {f1 = v1; ...; fn = vn; ...} 匹配并生成绑定集 ibi 。请注意,记录值可能具有比记录模式更多的字段。

As a syntactic sugar, another form of record pattern is provided: {f1; ...; fn}. It is desugared to {f1 = f1; ...; fn = fn}.
作为语法糖,提供了另一种形式的记录模式: {f1; ...; fn} 。它被脱糖为 {f1 = f1; ...; fn = fn}

3.4.2. Tuples 3.4.2. 元组 ¶

Like records, tuples are a composite of other types of data. But instead of naming the components, they are identified by position. Here are some examples of tuples:
与记录一样,元组是其他类型数据的组合。但不是命名组件,而是通过位置来标识它们。以下是元组的一些示例:

(1, 2, 10)
(true, "Hello")
([1; 2; 3], (0.5, 'X'))

A tuple with two components is called a pair. A tuple with three components is called a triple. Beyond that, we usually just use the word tuple instead of continuing a naming scheme based on numbers.
具有两个组件的元组称为对。具有三个组成部分的元组称为三元组。除此之外,我们通常只使用元组这个词,而不是继续基于数字的命名方案。

Tip

Beyond about three components, it’s arguably better to use records instead of tuples, because it becomes hard for a programmer to remember which component was supposed to represent what information.
除了大约三个组件之外,可以说使用记录而不是元组更好,因为程序员很难记住哪个组件应该代表什么信息。

Building of tuples is easy: just write the tuple, as above. Accessing again involves pattern matching, for example:
元组的构建很简单:只需编写元组即可,如上所示。再次访问涉及到模式匹配,例如:

match (1, 2, 3) with (x, y, z) -> x + y + z
- : int = 6

Syntax.

A tuple is written
写了一个元组

(e1, e2, ..., en)

The parentheses are not entirely mandatory —often your code can successfully parse without them— but they are usually considered to be good style to include.
括号并不完全是强制性的——通常你的代码不需要它们也可以成功解析——但它们通常被认为是包含它们的良好风格。

Dynamic semantics. 动态语义。

  • If for all i in 1..n it holds that ei ==> vi, then (e1, ..., en) ==> (v1, ..., vn).
    如果对于 1..n 中的所有 i 都包含 ei ==> vi ,则 (e1, ..., en) ==> (v1, ..., vn)

Static semantics. 静态语义。

Tuple types are written using a new type constructor *, which is different than the multiplication operator. The type t1 * ... * tn is the type of tuples whose first component has type t1, …, and nth component has type tn.
元组类型是使用新的类型构造函数 * 编写的,它与乘法运算符不同。类型 t1 * ... * tn 是元组的类型,其第一个组件的类型为 t1 ,...,第 n 个组件的类型为 tn

  • If for all i in 1..n it holds that ei : ti, then (e1, ..., en) : t1 * ... * tn.
    如果对于 1..n 中的所有 i 都包含 ei : ti ,则 (e1, ..., en) : t1 * ... * tn

Pattern matching. 模式匹配。

We add the following new pattern form to the list of legal patterns:
我们将以下新模式形式添加到合法模式列表中:

  • (p1, ..., pn)

The parentheses are again not entirely mandatory but usually are idimoatic to include.
括号也不是完全强制的,但通常是惯用的。

And we extend the definition of when a pattern matches a value and produces a binding as follows:
我们扩展了模式何时匹配值并生成绑定的定义,如下所示:

  • If for all i in 1..n, it holds that pi matches vi and produces bindings bi, then the tuple pattern (p1, ..., pn) matches the tuple value (v1, ..., vn) and produces the set ibi of bindings. Note that the tuple value must have exactly the same number of components as the tuple pattern does.
    如果对于 1..n 中的所有 i ,它认为 pi 匹配 vi 并生成绑定 bi ,那么元组模式 (p1, ..., pn) 与元组值 (v1, ..., vn) 匹配并生成绑定集 ibi 。请注意,元组值必须具有与元组模式完全相同的组件数量。

3.4.3. Variants vs. Tuples and Records
3.4.3. 变体 vs 元组和记录 ¶

Note 笔记

The second video above uses more advanced examples of variants that will be studied in a later section.
上面的第二个视频使用了更高级的变体示例,这些示例将在后面的部分中进行研究。

The big difference between variants and the types we just learned (records and tuples) is that a value of a variant type is one of a set of possibilities, whereas a value of a tuple or record type provides each of a set of possibilities. Going back to our examples, a value of type day is one of Sun or Mon or etc. But a value of type mon provides each of a string and an int and ptype. Note how, in those previous two sentences, the word “or” is associated with variant types, and the word “and” is associated with tuple and record types. That’s a good clue if you’re ever trying to decide whether you want to use a variant, or a tuple or record: if you need one piece of data or another, you want a variant; if you need one piece of data and another, you want a tuple or record.
变体和我们刚刚学到的类型(记录和元组)之间的最大区别在于,变体类型的值是一组可能性中的一个,而元组或记录类型的值提供了一组可能性中的每一个。回到我们的示例, day 类型的值是 SunMon 等之一。但是 mon 类型的值提供 stringintptype 中的每一个。请注意,在前两个句子中,单词“or”与变体类型相关联,而单词“and”与元组和记录类型相关联。如果您试图决定是否要使用变体、元组或记录,这是一个很好的线索:如果您需要一个数据或另一个数据,您需要一个变体;如果您需要一个数据和另一个数据,您需要一个元组或记录。

One-of types are more commonly known as sum types, and each-of types as product types. Those names come from set theory. Variants are like disjoint union, because each value of a variant comes from one of many underlying sets (and thus far each of those sets is just a single constructor hence has cardinality one). Disjoint union is indeed sometimes written with a summation operator Σ. Tuples/records are like Cartesian product, because each value of a tuple or record contains a value from each of many underlying sets. Cartesian product is usually written with a product operator, × or Π.
其中一种类型通常称为求和类型,而每种类型通常称为乘积类型。这些名字来自集合论。变体就像不相交的联合,因为变体的每个值都来自许多基础集合之一(到目前为止,这些集合中的每一个都只是一个构造函数,因此基数为一)。不相交的联合有时确实是用求和运算符 Σ 编写的。元组/记录就像笛卡尔积,因为元组或记录的每个值都包含来自许多基础集合中的每个值。笛卡尔积通常用乘积运算符 ×Π 编写。

3.5. Advanced Pattern Matching
3.5. 模式匹配高级 ¶

Here are some additional pattern forms that are useful:
以下是一些有用的附加模式形式:

  • p1 | ... | pn: an “or” pattern; matching against it succeeds if a match succeeds against any of the individual patterns pi, which are tried in order from left to right. All the patterns must bind the same variables.
    p1 | ... | pn :“或”模式;如果与任何单个模式 pi 匹配成功,则匹配成功,这些模式按从左到右的顺序尝试。所有模式必须绑定相同的变量。

  • (p : t): a pattern with an explicit type annotation.
    (p : t) :具有显式类型注释的模式。

  • c: here, c means any constant, such as integer literals, string literals, and booleans.
    c :这里, c 表示任何常量,例如整数文字、字符串文字和布尔值。

  • 'ch1'..'ch2': here, ch means a character literal. For example, 'A'..'Z' matches any uppercase letter.
    'ch1'..'ch2' :这里, ch 表示字符文字。例如, 'A'..'Z' 匹配任何大写字母。

  • p when e: matches p but only if e evaluates to true.
    p when e :匹配 p 但仅当 e 计算结果为 true 时。

You can read about all the pattern forms in the manual.
您可以在手册中阅读有关所有图案形式的信息。

3.5.1. Pattern Matching with Let
3.5.1. Let 上的模式匹配 ¶

The syntax we’ve been using so far for let expressions is, in fact, a special case of the full syntax that OCaml permits. That syntax is:
事实上,到目前为止,我们一直在 let 表达式中使用的语法是 OCaml 允许的完整语法的一种特殊情况。该语法是:

let p = e1 in e2

That is, the left-hand side of the binding may in fact be a pattern, not just an identifier. Of course, variable identifiers are on our list of valid patterns, so that’s why the syntax we’ve studied so far is just a special case.
也就是说,绑定的左侧实际上可能是一个模式,而不仅仅是一个标识符。当然,变量标识符在我们的有效模式列表中,因此这就是为什么我们到目前为止研究的语法只是一个特殊情况。

Given this syntax, we revisit the semantics of let expressions.
考虑到这种语法,我们重新审视 let 表达式的语义。

Dynamic semantics. 动态语义。

To evaluate let p = e1 in e2: 评估 let p = e1 in e2

  1. Evaluate e1 to a value v1.
    e1 计算为值 v1

  2. Match v1 against pattern p. If it doesn’t match, raise the exception Match_failure. Otherwise, if it does match, it produces a set b of bindings.
    v1 与模式 p 匹配。如果不匹配,则引发异常 Match_failure 。否则,如果匹配,则会生成一组 b 绑定。

  3. Substitute those bindings b in e2, yielding a new expression e2'.
    将这些绑定替换为 e2 中的 b ,产生新的表达式 e2'

  4. Evaluate e2' to a value v2.
    e2' 计算为值 v2

  5. The result of evaluating the let expression is v2.
    let 表达式的计算结果是 v2

Static semantics. 静态语义。

  • If all the following hold then (let p = e1 in e2) : t2:
    如果以下所有条件成立,则 (let p = e1 in e2) : t2

    • e1 : t1

    • the pattern variables in p are x1..xn
      p 中的模式变量是 x1..xn

    • e2 : t2 under the assumption that for all i in 1..n it holds that xi : ti,
      e2 : t2 假设对于 1..n 中的所有 i 都满足 xi : ti

Let definitions. 让定义。

As before, a let definition can be understood as a let expression whose body has not yet been given. So their syntax can be generalized to
和之前一样,let 定义可以理解为一个尚未给出主体的 let 表达式。所以他们的语法可以概括为

let p = e

and their semantics follow from the semantics of let expressions, as before.
和以前一样,它们的语义遵循 let 表达式的语义。

3.5.2. Pattern Matching with Functions
3.5.2. 函数上的模式匹配 ¶

The syntax we’ve been using so far for functions is also a special case of the full syntax that OCaml permits. That syntax is:
到目前为止我们使用的函数语法也是 OCaml 允许的完整语法的一个特例。该语法是:

let f p1 ... pn = e1 in e2   (* function as part of let expression *)
let f p1 ... pn = e          (* function definition at toplevel *)
fun p1 ... pn -> e           (* anonymous function *)

The truly primitive syntactic form we need to care about is fun p -> e. Let’s revisit the semantics of anonymous functions and their application with that form; the changes to the other forms follow from those below:
我们需要关心的真正原始的语法形式是 fun p -> e 。让我们重新审视匿名函数的语义及其在该形式中的应用;其他表格的变化如下:

Static semantics. 静态语义。

  • Let x1..xn be the pattern variables appearing in p. If by assuming that x1 : t1 and x2 : t2 and … and xn : tn, we can conclude that p : t and e :u, then fun p -> e : t -> u.
    x1..xn 作为 p 中出现的模式变量。如果通过假设 x1 : t1x2 : t2 以及 ... 和 xn : tn ,我们可以得出 p : t e :u 的结论,则 fun p -> e : t -> u

  • The type checking rule for application is unchanged.
    应用程序的类型检查规则不变。

Dynamic semantics. 动态语义。

  • The evaluation rule for anonymous functions is unchanged.
    匿名函数的求值规则不变。

  • To evaluate e0 e1: 评估 e0 e1

    1. Evaluate e0 to an anonymous function fun p -> e, and evaluate e1 to value v1.
      e0 算作匿名函数 fun p -> e ,并将 e1 算作值 v1

    2. Match v1 against pattern p. If it doesn’t match, raise the exception Match_failure. Otherwise, if it does match, it produces a set b of bindings.
      v1 去匹配模式 p 。如果不匹配,则引发异常 Match_failure 。否则,如果匹配,则会生成一组 b 绑定。

    3. Substitute those bindings b in e, yielding a new expression e'.
      将这些绑定替换为 e 中的 b ,产生新的表达式 e'

    4. Evaluate e' to a value v, which is the result of evaluating e0 e1.
      e' 算作为值 v ,这就是计算 e0 e1 的结果了。

3.5.3. Pattern Matching Examples
3.5.3. 模式匹配示例 ¶

Here are several ways to get a Pokemon’s hit points:
以下是获得神奇宝贝 HP 的几种方法:

(* Pokemon types *)
type ptype = TNormal | TFire | TWater

(* A record to represent Pokemon *)
type mon = { name : string; hp : int; ptype : ptype }

(* OK *)
let get_hp m = match m with { name = n; hp = h; ptype = t } -> h

(* better *)
let get_hp m = match m with { name = _; hp = h; ptype = _ } -> h

(* better *)
let get_hp m = match m with { name; hp; ptype } -> hp

(* better *)
let get_hp m = match m with { hp } -> hp

(* best *)
let get_hp m = m.hp
type ptype = TNormal | TFire | TWater
type mon = { name : string; hp : int; ptype : ptype; }
val get_hp : mon -> int = <fun>
val get_hp : mon -> int = <fun>
val get_hp : mon -> int = <fun>
val get_hp : mon -> int = <fun>
val get_hp : mon -> int = <fun>

Here’s how to get the first and second components of a pair:
以下是获取一对的第一个和第二个组件的方法:

let fst (x, _) = x

let snd (_, y) = y
val fst : 'a * 'b -> 'a = <fun>
val snd : 'a * 'b -> 'b = <fun>

Both fst and snd are actually already defined for you in the standard library.
fstsnd 实际上已经在标准库中为您定义了。

Finally, here are several ways to get the 3rd component of a triple:
最后,这里有几种获取三元组的第三个分量的方法:

(* OK *)
let thrd t = match t with x, y, z -> z

(* good *)
let thrd t =
  let x, y, z = t in
  z

(* better *)
let thrd t =
  let _, _, z = t in
  z

(* best *)
let thrd (_, _, z) = z
val thrd : 'a * 'b * 'c -> 'c = <fun>
val thrd : 'a * 'b * 'c -> 'c = <fun>
val thrd : 'a * 'b * 'c -> 'c = <fun>
val thrd : 'a * 'b * 'c -> 'c = <fun>

The standard library does not define any functions for triples, quadruples, etc.
标准库没有定义任何三元组、四元组等函数。

3.6. Type Synonyms 3.6. 类型同义词 ¶

A type synonym is a new name for an already existing type. For example, here are some type synonyms that might be useful in representing some types from linear algebra:
类型同义词是现有类型的新名称。例如,以下是一些类型同义词,它们可能有助于表示线性代数中的某些类型:

type point = float * float
type vector = float list
type matrix = float list list
type point = float * float
type vector = float list
type matrix = float list list

Anywhere that a float * float is expected, you could use point, and vice-versa. The two are completely exchangeable for one another. In the following code, get_x doesn’t care whether you pass it a value that is annotated as one vs. the other:
在任何需要 float * float 的地方,您都可以使用 point ,反之亦然。两者是完全可以互换的。在下面的代码中, get_x 并不关心您是否向其传递一个被注释为一个与另一个的值:

let get_x = fun (x, _) -> x

let p1 : point = (1., 2.)
let p2 : float * float = (1., 3.)

let a = get_x p1
let b = get_x p2
val get_x : 'a * 'b -> 'a = <fun>
val p1 : point = (1., 2.)
val p2 : float * float = (1., 3.)
val a : float = 1.
val b : float = 1.

Type synonyms are useful because they let us give descriptive names to complex types. They are a way of making code more self-documenting.
类型同义词很有用,因为它们让我们可以为复杂类型提供描述性名称。它们是使代码更加自我记录的一种方式。

3.7. Options 3.7. 可选型 ¶

Suppose you want to write a function that usually returns a value of type t, but sometimes returns nothing. For example, you might want to define a function list_max that returns the maximum value in a list, but there’s not a sensible thing to return on an empty list:
假设您要编写一个通常返回 t 类型值的函数,但有时不返回任何内容。例如,您可能想要定义一个函数 list_max 返回列表中的最大值,但在空列表上返回并不明智:

let rec list_max = function
  | [] -> ???
  | h :: t -> max h (list_max t)

There are a couple possibilities to consider:
有几种可能性需要考虑:

  • Return min_int? But then list_max will only work for integers— not floats or other types.
    返回 min_int ?但是 list_max 仅适用于整数,不适用于浮点数或其他类型。

  • Raise an exception? But then the user of the function has to remember to catch the exception.
    提出例外?但是函数的用户必须记住捕获异常。

  • Return null? That works in Java, but by design OCaml does not have a null value. That’s actually a good thing: null pointer bugs are not fun to debug.
    返回 null ?这在 Java 中有效,但根据设计,OCaml 没有 null 值。这实际上是一件好事:空指针错误调试起来并不有趣。

Note 笔记

Sir Tony Hoare calls his invention of null a “billion-dollar mistake”.
托尼·霍尔爵士称他的 null 发明是一个“价值数十亿美元的错误”。

In addition to those possibilities, OCaml provides something even better called an option. (Haskellers will recognize options as the Maybe monad.)
除了这些可能性之外,OCaml 还提供了更好的东西,称为可选型。( Haskeller 会将选项视为 Maybe monad )

You can think of an option as being like a closed box. Maybe there’s something inside the box, or maybe box is empty. We don’t know which until we open the box. If there turns out to be something inside the box when we open it, we can take that thing out and use it. Thus, options provide a kind of “maybe type,” which ultimately is a kind of one-of type: the box is in one of two states, full or empty.
您可以将选项型视为一个封闭的盒子。也许盒子里面有东西,或者盒子是空的。在打开盒子之前我们不知道是哪一个。如果我们打开盒子时发现里面有东西,我们就可以把它拿出来使用。因此,选项提供了一种“可能类型”,最终是一种单选类型:盒子处于两种状态之一:满或空。

In list_max above, we’d like to metaphorically return a box that’s empty if the list is empty, or a box that contains the maximum element of the list if the list is non empty.
在上面的 list_max 中,如果列表为空,我们想隐喻地返回一个空的框,或者如果列表非空,则返回一个包含列表最大元素的框。

Here’s how we create an option that is like a box with 42 inside it:
以下是我们如何创建一个类似于里面有 42 的盒子的选项:

Some 42
- : int option = Some 42

And here’s how we create an option that is like an empty box:
以下是我们如何创建一个像空框一样的选项:

None
- : 'a option = None

The Some means there’s something inside the box, and it’s 42. The None means there’s nothing inside the box.
Some 表示盒子里面有东西,它是 42None 表示盒子内没有任何东西。

Like list, we call option a type constructor: given a type, it produces a new type; but, it is not itself a type. So for any type t, we can write t option as a type. But option all by itself cannot be used as a type. Values of type t option might contain a value of type t, or they might contain nothing. None has type 'a option because it’s unconstrained what the type is of the thing inside — as there isn’t anything inside.
list 一样,我们将 option 称为类型构造函数:给定一个类型,它会生成一个新类型;但是,它本身并不是一种类型。因此,对于任何类型 t ,我们都可以将 t option 写为类型。但是 option 本身不能用作类型。 t option 类型的值可能包含 t 类型的值,也可能不包含任何内容。 None 具有类型 'a option ,因为它不受内部事物类型的限制——因为内部没有任何东西。

You can access the contents of an option value e using pattern matching. Here’s a function that extracts an int from an option, if there is one inside, and converts it to a string:
您可以使用模式匹配来访问选项值 e 的内容。这是一个从选项中提取 int 的函数(如果里面有的话),并将其转换为字符串:

let extract o =
  match o with
  | Some i -> string_of_int i
  | None -> "";;
val extract : int option -> string = <fun>

And here are a couple of example usages of that function:
以下是该函数的几个示例用法:

extract (Some 42);;
extract None;;
- : string = "42"
- : string = ""

Here’s how we can write list_max with options:
下面是我们如何使用选项编写 list_max

let rec list_max = function
  | [] -> None
  | h :: t -> begin
      match list_max t with
        | None -> Some h
        | Some m -> Some (max h m)
      end
val list_max : 'a list -> 'a option = <fun>

Tip

The begin..end wrapping the nested pattern match above is not strictly required here but is not a bad habit, as it will head off potential syntax errors in more complicated code. The keywords begin and end are equivalent to ( and ).
此处并不严格要求使用 begin .. end 包装上面的嵌套模式匹配,但这并不是一个坏习惯,因为它将阻止更复杂的代码中潜在的语法错误。关键字 beginend 相当于 ()

In Java, every object reference is implicitly an option. Either there is an object inside the reference, or there is nothing there. That “nothing” is represented by the value null. Java does not force programmers to explicitly check for the null case, which leads to null pointer exceptions. OCaml options force the programmer to include a branch in the pattern match for None, thus guaranteeing that the programmer thinks about the right thing to do when there’s nothing there. So we can think of options as a principled way of eliminating null from the language. Using options is usually considered better coding practice than raising exceptions, because it forces the caller to do something sensible in the None case.
在 Java 中,每个对象引用都是隐式的一个选项。引用中要么有一个对象,要么什么都没有。 “无”由值 null 表示。 Java 不强制程序员显式检查 null 情况,这会导致空指针异常。 OCaml 选项强制程序员在 None 的模式匹配中包含一个分支,从而保证程序员在没有任何内容时考虑要做正确的事情。因此,我们可以将选项视为从语言中消除 null 的原则性方法。使用选项通常被认为比引发异常更好的编码实践,因为它迫使调用者在 None 情况下做一些明智的事情。

Syntax and semantics of options.
选项的语法和语义。

  • t option is a type for every type t.
    t option 是每个类型 t 的类型。

  • None is a value of type 'a option.
    None'a option 类型的值。

  • Some e is an expression of type t option if e : t. If e ==> v then Some e ==> Some v
    如果 e : t ,则 Some et option 类型的表达式。如果 e ==> vSome e ==> Some v

3.8. Association Lists 3.8. 关联列表 ¶

A map is a data structure that maps keys to values. Maps are also known as dictionaries. One easy implementation of a map is an association list, which is a list of pairs. Here, for example, is an association list that maps some shape names to the number of sides they have:
映射是将键映射到值的数据结构。映射也称为字典。映射的一个简单实现是关联列表,它是(键值)对的列表。例如,下面是一个关联列表,它将一些形状名称映射到它们具有的边数:

let d = [("rectangle", 4); ("nonagon", 9); ("icosagon", 20)]
val d : (string * int) list =
  [("rectangle", 4); ("nonagon", 9); ("icosagon", 20)]

Note that an association list isn’t so much a built-in data type in OCaml as a combination of two other types: lists and pairs.
请注意,关联列表与其说是 OCaml 中的内置数据类型,不如说是其他两种类型的组合:列表和对。

Here are two functions that implement insertion and lookup in an association list:
以下是在关联列表中实现插入和查找的两个函数:

(** [insert k v lst] is an association list that binds key [k] to value [v]
    and otherwise is the same as [lst] *)
let insert k v lst = (k, v) :: lst

(** [lookup k lst] is [Some v] if association list [lst] binds key [k] to
    value [v]; and is [None] if [lst] does not bind [k]. *)
let rec lookup k = function
| [] -> None
| (k', v) :: t -> if k = k' then Some v else lookup k t
val insert : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) list = <fun>
val lookup : 'a -> ('a * 'b) list -> 'b option = <fun>

The insert function simply adds a new map from a key to a value at the front of the list. It doesn’t bother to check whether the key is already in the list. The lookup function looks through the list from left to right. So if there did happen to be multiple maps for a given key in the list, only the most recently inserted one would be returned.
insert 函数只是添加一个从键到列表前面的值的新映射。它不费心检查密钥是否已经在列表中。 lookup 函数从左到右查看列表。因此,如果列表中的给定键确实有多个映射,则只会返回最近插入的映射。

Insertion in an association list is therefore constant time, and lookup is linear time. Although there are certainly more efficient implementations of dictionaries—and we’ll study some later in this course—association lists are a very easy and useful implementation for small dictionaries that aren’t performance critical. The OCaml standard library has functions for association lists in the List module; look for List.assoc and the functions below it in the documentation. What we just wrote as lookup is actually already defined as List.assoc_opt. There is no pre-defined insert function in the library because it’s so trivial just to cons a pair on.
因此,关联列表中的插入是常数时间,而查找是线性时间。尽管肯定有更有效的字典实现(我们将在本课程后面研究一些),但对于性能不关键的小型字典来说,关联列表是一种非常简单且有用的实现。 OCaml标准库在 List 模块中有关联列表的函数;在文档中查找 List.assoc 及其下面的函数。我们刚刚写的 lookup 实际上已经定义为 List.assoc_opt 。库中没有预定义的 insert 函数,因为仅使用一对就非常简单了。

3.9. Algebraic Data Types
3.9. 代数数据类型 ¶

Thus far, we have seen variants simply as enumerating a set of constant values, such as:
到目前为止,我们看到的不定型(变体)只是枚举一组常量值,例如:

type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat

type ptype = TNormal | TFire | TWater

type peff = ENormal | ENotVery | Esuper

But variants are far more powerful than this.
但变种的威力远比这强大。

3.9.1. Variants that Carry Data
3.9.1. 携带数据的不定型 ¶

As a running example, here is a variant type shape that does more than just enumerate values:
作为一个正在运行的示例,下面是一个变体类型 shape ,它的作用不仅仅是枚举值:

type point = float * float
type shape =
  | Point of point
  | Circle of point * float (* center and radius *)
  | Rect of point * point (* lower-left and upper-right corners *)
type point = float * float
type shape = Point of point | Circle of point * float | Rect of point * point

This type, shape, represents a shape that is either a point, a circle, or a rectangle. A point is represented by a constructor Point that carries some additional data, which is a value of type point. A circle is represented by a constructor Circle that carries two pieces of data: one of type point and the other of type float. Those data represent the center of the circle and its radius. A rectangle is represented by a constructor Rect that carries two more points.
此类型 shape 表示点、圆或矩形的形状。点由构造函数 Point 表示,该构造函数携带一些附加数据,这些数据是 point 类型的值。圆由构造函数 Circle 表示,该构造函数携带两部分数据:一个为 point 类型,另一个为 float 类型。这些数据代表圆的中心及其半径。矩形由带有另外两个点的构造函数 Rect 表示。

Here are a couple functions that use the shape type:
以下是使用 shape 类型的几个函数:

let area = function
  | Point _ -> 0.0
  | Circle (_, r) -> Float.pi *. (r ** 2.0)
  | Rect ((x1, y1), (x2, y2)) ->
      let w = x2 -. x1 in
      let h = y2 -. y1 in
      w *. h

let center = function
  | Point p -> p
  | Circle (p, _) -> p
  | Rect ((x1, y1), (x2, y2)) -> ((x2 +. x1) /. 2.0, (y2 +. y1) /. 2.0)
val area : shape -> float = <fun>
val center : shape -> point = <fun>

The shape variant type is the same as those we’ve seen before in that it is defined in terms of a collection of constructors. What’s different than before is that those constructors carry additional data along with them. Every value of type shape is formed from exactly one of those constructors. Sometimes we call the constructor a tag, because it tags the data it carries as being from that particular constructor.
shape 变体类型与我们之前看到的相同,因为它是根据构造函数集合定义的。与以前不同的是,这些构造函数附带了额外的数据。 shape 类型的每个值都是由这些构造函数之一形成的。有时我们将构造函数称为标签,因为它将其携带的数据标记为来自该特定构造函数。

Variant types are sometimes called tagged unions. Every value of the type is from the set of values that is the union of all values from the underlying types that the constructor carries. For example, with the shape type, every value is tagged with either Point or Circle or Rect and carries a value from:
变体类型有时称为标记联合。该类型的每个值都来自值集,该值集是构造函数携带的基础类型的所有值的并集。例如,对于 shape 类型,每个值都用 PointCircleRect 标记,并带有以下值:

  • the set of all point values, unioned with
    所有 point 值的集合,与

  • the set of all point * float values, unioned with
    所有 point * float 值的集合,与

  • the set of all point * point values.
    所有 point * point 值的集合。

Another name for these variant types is an algebraic data type. “Algebra” here refers to the fact that variant types contain both sum and product types, as defined in the previous lecture. The sum types come from the fact that a value of a variant is formed by one of the constructors. The product types come from that fact that a constructor can carry tuples or records, whose values have a sub-value from each of their component types.
这些变体类型的另一个名称是代数数据类型。这里的“代数”指的是变体类型同时包含和类型和乘积类型,如上一讲中所定义的。 sum 类型来自这样一个事实:变体的值是由构造函数之一形成的。产品类型来自这样一个事实:构造函数可以携带元组或记录,其值具有来自其每个组件类型的子值。

Using variants, we can express a type that represents the union of several other types, but in a type-safe way. Here, for example, is a type that represents either a string or an int:
使用变体,我们可以以类型安全的方式表达表示多个其他类型的联合的类型。例如,这里是表示 stringint 的类型:

type string_or_int =
  | String of string
  | Int of int
type string_or_int = String of string | Int of int

If we wanted to, we could use this type to code up lists (e.g.) that contain either strings or ints:
如果我们愿意,我们可以使用这种类型来编码包含字符串或整数的列表(例如):

type string_or_int_list = string_or_int list

let rec sum : string_or_int list -> int = function
  | [] -> 0
  | String s :: t -> int_of_string s + sum t
  | Int i :: t -> i + sum t

let lst_sum = sum [String "1"; Int 2]
type string_or_int_list = string_or_int list
val sum : string_or_int list -> int = <fun>
val lst_sum : int = 3

Variants thus provide a type-safe way of doing something that might before have seemed impossible.
因此,变体提供了一种类型安全的方法来完成以前看似不可能的事情。

Variants also make it possible to discriminate which tag a value was constructed with, even if multiple constructors carry the same type. For example:
变体还可以区分值是用哪个标签构造的,即使多个构造函数携带相同的类型。例如:

type t = Left of int | Right of int
let x = Left 1
let double_right = function
  | Left i -> i
  | Right i -> 2 * i
type t = Left of int | Right of int
val x : t = Left 1
val double_right : t -> int = <fun>

3.9.2. Syntax and Semantics
3.9.2. 语法和语义 ¶

Syntax.

To define a variant type:
要定义变体类型:

type t = C1 [of t1] | ... | Cn [of tn]

The square brackets above denote that of ti is optional. Every constructor may individually either carry no data or carry data. We call constructors that carry no data constant; and those that carry data, non-constant.
上面的方括号表示 of ti 是可选的。每个构造函数可以单独地不携带数据或携带数据。我们调用不携带数据常量的构造函数;那些携带数据的,非恒定的。

To write an expression that is a variant:
要编写一个变体表达式:

C e

Or:

C

depending on whether the constructor name C is non-constant or constant.
取决于构造函数名称 C 是非常量还是常量。

Dynamic semantics. 动态语义。

  • If e==>v then C e ==> C v, assuming C is non-constant.
    e==>vC e ==> C v ,只要 C 是非常量。

  • C is already a value, assuming C is constant.
    只要 C 是常数, C 已经是一个值。

Static semantics. 静态语义。

  • If t = ... | C | ... then C : t.
    t = ... | C | ...C : t

  • If t = ... | C of t' | ... and if e : t' then C e : t.
    t = ... | C of t' | ... 且若 e : t'C e : t

Pattern matching. 模式匹配。

We add the following new pattern form to the list of legal patterns:
我们将以下新模式形式添加到合法模式列表中:

  • C p

And we extend the definition of when a pattern matches a value and produces a binding as follows:
我们扩展了模式何时匹配值并生成绑定的定义,如下所示:

  • If p matches v and produces bindings b, then C p matches C v and produces bindings b.
    p 匹配 v 并生成绑定 b ,则 C p 匹配 C v 并生成绑定 b

3.9.3. Catch-all Cases 3.9.3. 包罗万象匹配 ¶

One thing to beware of when pattern matching against variants is what Real World OCaml calls “catch-all cases”. Here’s a simple example of what can go wrong. Let’s suppose you write this variant and function:
当针对变体进行模式匹配时需要注意的一件事是现实世界 OCaml 所说的“包罗万象的情况”。这是一个可能出错的简单示例。假设您编写了这个变体和函数:

type color = Blue | Red

(* a thousand lines of code in between *)

let string_of_color = function
  | Blue -> "blue"
  | _ -> "red"
type color = Blue | Red
val string_of_color : color -> string = <fun>

Seems fine, right? But then one day you realize there are more colors in the world. You need to represent green. So you go back and add green to your variant:
看起来不错,对吧?但有一天你意识到世界上还有更多的颜色。你需要代表绿色。所以你返回并在你的变体中添加绿色:

type color = Blue | Red | Green

(* a thousand lines of code in between *)

let string_of_color = function
  | Blue -> "blue"
  | _ -> "red"
type color = Blue | Red | Green
val string_of_color : color -> string = <fun>

But because of the thousand lines of code in between, you forget that string_of_color needs updating. And now, all the sudden, you are red-green color blind:
但由于中间有数千行代码,您忘记了 string_of_color 需要更新。现在,突然间,您变成了红绿色盲:

string_of_color Green
- : string = "red"

The problem is the catch-all case in the pattern match inside string_of_color: the final case that uses the wildcard pattern to match anything. Such code is not robust against future changes to the variant type.
问题是 string_of_color 内模式匹配中的包罗万象的情况:使用通配符模式匹配任何内容的最终情况。此类代码对于变体类型的未来更改并不稳健。

If, instead, you had originally coded the function as follows, life would be better:
相反,如果您最初按如下方式编写该函数,那么生活会更好:

let string_of_color = function
  | Blue -> "blue"
  | Red  -> "red"
File "[9]", lines 1-3, characters 22-17:
1 | ......................function
2 |   | Blue -> "blue"
3 |   | Red  -> "red"
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Green
val string_of_color : color -> string = <fun>

The OCaml type checker now alerts you that you haven’t yet updated string_of_color to account for the new constructor.
OCaml 类型检查器现在会提醒您尚未更新 string_of_color 以考虑新的构造函数。

The moral of the story is: catch-all cases lead to buggy code. Avoid using them.
这个故事的寓意是:包罗万象的情况会导致有错误的代码。避免使用它们。

3.9.4. Recursive Variants
3.9.4. 递归变体 ¶

Variant types may mention their own name inside their own body. For example, here is a variant type that could be used to represent something similar to int list:
变种类型可能会在自己的某部分定义内提到自身的名称。例如,下面是一个变种类型,可用于表示类似于 int list 的内容:

type intlist = Nil | Cons of int * intlist

let lst3 = Cons (3, Nil)  (* similar to 3 :: [] or [3]*)
let lst123 = Cons(1, Cons(2, lst3)) (* similar to [1; 2; 3] *)

let rec sum (l : intlist) : int=
  match l with
  | Nil -> 0
  | Cons (h, t) -> h + sum t

let rec length : intlist -> int = function
  | Nil -> 0
  | Cons (_, t) -> 1 + length t

let empty : intlist -> bool = function
  | Nil -> true
  | Cons _ -> false
type intlist = Nil | Cons of int * intlist
val lst3 : intlist = Cons (3, Nil)
val lst123 : intlist = Cons (1, Cons (2, Cons (3, Nil)))
val sum : intlist -> int = <fun>
val length : intlist -> int = <fun>
val empty : intlist -> bool = <fun>

Notice that in the definition of intlist, we define the Cons constructor to carry a value that contains an intlist. This makes the type intlist be recursive: it is defined in terms of itself.
请注意,在 intlist 的定义中,我们定义 Cons 构造函数来携带包含 intlist 的值。这使得类型 intlist 是递归的:它是根据自身定义的。

Types may be mutually recursive if you use the and keyword:
如果使用 and 关键字,类型可能会相互递归:

type node = {value : int; next : mylist}
and mylist = Nil | Node of node
type node = { value : int; next : mylist; }
and mylist = Nil | Node of node

Any such mutual recursion must involve at least one variant or record type that the recursion “goes through”. For example, the following is not allowed:
任何此类相互递归必须至少涉及递归“经过”的一种变体或记录类型。例如,以下行为是不允许的:

type t = u and u = t
File "[12]", line 1, characters 0-10:
1 | type t = u and u = t
    ^^^^^^^^^^
Error: The definition of t contains a cycle:
       u

But this is: 但这是:

type t = U of u and u = T of t
type t = U of u
and u = T of t

Record types may also be recursive:
记录类型也可以是递归的:

type node = {value : int; next : node}
type node = { value : int; next : node; }

But plain old type synonyms may not be:
但普通的旧类型同义词可能不是:

type t = t * t
File "[15]", line 1, characters 0-14:
1 | type t = t * t
    ^^^^^^^^^^^^^^
Error: The type abbreviation t is cyclic

Although node is a legal type definition, there is no way to construct a value of that type because of the circularity involved: to construct the very first node value in existence, you would already need a value of type node to exist. Later, when we cover imperative features, we’ll see a similar idea used (but successfully) for mutable linked lists.
尽管 node 是合法的类型定义,但由于涉及循环性,无法构造该类型的值:要构造现有的第一个 node 值,您已经需要存在 node 类型的值。稍后,当我们介绍命令式功能时,我们将看到类似的想法(但成功地)用于可变链表。

3.9.5. Parameterized Variants
3.9.5. 有参数的变种 ¶

Variant types may be parameterized on other types. For example, the intlist type above could be generalized to provide lists (coded up ourselves) over any type:
变体类型可以在其他类型上参数化。例如,上面的 intlist 类型可以概括为提供任何类型的列表(我们自己编码):

type 'a mylist = Nil | Cons of 'a * 'a mylist

let lst3 = Cons (3, Nil)  (* similar to [3] *)
let lst_hi = Cons ("hi", Nil)  (* similar to ["hi"] *)
type 'a mylist = Nil | Cons of 'a * 'a mylist
val lst3 : int mylist = Cons (3, Nil)
val lst_hi : string mylist = Cons ("hi", Nil)

Here, mylist is a type constructor but not a type: there is no way to write a value of type mylist. But we can write value of type int mylist (e.g., lst3) and string mylist (e.g., lst_hi). Think of a type constructor as being like a function, but one that maps types to types, rather than values to value.
这里, mylist 是类型构造函数,但不是类型:无法编写 mylist 类型的值。但是我们可以编写 int mylist 类型的值(例如 lst3 )和 string mylist (例如 lst_hi )。将类型构造函数视为函数,但将类型映射到类型,而不是将值映射到值。

Here are some functions over 'a mylist:
以下是 'a mylist 的一些功能:

let rec length : 'a mylist -> int = function
  | Nil -> 0
  | Cons (_, t) -> 1 + length t

let empty : 'a mylist -> bool = function
  | Nil -> true
  | Cons _ -> false
val length : 'a mylist -> int = <fun>
val empty : 'a mylist -> bool = <fun>

Notice that the body of each function is unchanged from its previous definition for intlist. All that we changed was the type annotation. And that could even be omitted safely:
请注意,每个函数的主体与之前的 intlist 定义相比没有变化。我们改变的只是类型注释。甚至可以安全地省略它:

let rec length = function
  | Nil -> 0
  | Cons (_, t) -> 1 + length t

let empty = function
  | Nil -> true
  | Cons _ -> false
val length : 'a mylist -> int = <fun>
val empty : 'a mylist -> bool = <fun>

The functions we just wrote are an example of a language feature called parametric polymorphism. The functions don’t care what the 'a is in 'a mylist, hence they are perfectly happy to work on int mylist or string mylist or any other (whatever) mylist. The word “polymorphism” is based on the Greek roots “poly” (many) and “morph” (form). A value of type 'a mylist could have many forms, depending on the actual type 'a.
我们刚刚编写的函数是称为参数多态性的语言功能的示例。这些函数不关心 'a mylist 中的 'a 是什么,因此它们非常乐意在 int myliststring mylist 或任何其他 (whatever) mylist 。 “多态性”一词源自希腊语词根“poly”(许多)和“morph”(形式)。 'a mylist 类型的值可以有多种形式,具体取决于实际类型 'a

As soon, though, as you place a constraint on what the type 'a might be, you give up some polymorphism. For example,
不过,当您对 'a 类型施加约束时,您就放弃了一些多态性。例如,

let rec sum = function
  | Nil -> 0
  | Cons (h, t) -> h + sum t
val sum : int mylist -> int = <fun>

The fact that we use the ( + ) operator with the head of the list constrains that head element to be an int, hence all elements must be int. That means sum must take in an int mylist, not any other kind of 'a mylist.
事实上,我们将 ( + ) 运算符与列表的头部一起使用,该头部元素限制为 int ,因此所有元素都必须是 int 。这意味着 sum 必须采用 int mylist ,而不是任何其他类型的 'a mylist

It is also possible to have multiple type parameters for a parameterized type, in which case parentheses are needed:
参数化类型也可以有多个类型参数,在这种情况下需要括号:

type ('a, 'b) pair = {first : 'a; second : 'b}
let x = {first = 2; second = "hello"}
type ('a, 'b) pair = { first : 'a; second : 'b; }
val x : (int, string) pair = {first = 2; second = "hello"}

3.9.6. Polymorphic Variants
3.9.6. 多态变体 ¶

Thus far, whenever you’ve wanted to define a variant type, you have had to give it a name, such as day, shape, or 'a mylist:
到目前为止,每当您想要定义变体类型时,都必须为其指定一个名称,例如 dayshape'a mylist

type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat

type shape =
  | Point of point
  | Circle of point * float
  | Rect of point * point

type 'a mylist = Nil | Cons of 'a * 'a mylist
type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
type shape = Point of point | Circle of point * float | Rect of point * point
type 'a mylist = Nil | Cons of 'a * 'a mylist

Occasionally, you might need a variant type only for the return value of a single function. For example, here’s a function f that can either return an int or ; you are forced to define a variant type to represent that result:
有时,您可能只需要单个函数的返回值的变体类型。例如,这里有一个函数 f ,它可以返回 int ;您被迫定义一个变体类型来表示该结果:

type fin_or_inf = Finite of int | Infinity

let f = function
  | 0 -> Infinity
  | 1 -> Finite 1
  | n -> Finite (-n)
type fin_or_inf = Finite of int | Infinity
val f : int -> fin_or_inf = <fun>

The downside of this definition is that you were forced to defined fin_or_inf even though it won’t be used throughout much of your program.
此定义的缺点是您被迫定义 fin_or_inf ,即使它不会在程序的大部分内容中使用。

There’s another kind of variant in OCaml that supports this kind of programming: polymorphic variants. Polymorphic variants are just like variants, except:
OCaml 中还有另一种支持这种编程的变体:多态变体。多态变体与变体一样,除了:

  1. You don’t have declare their type or constructors before using them.
    在使用它们之前,您无需声明它们的类型或构造函数。

  2. There is no name for a polymorphic variant type. (So another name for this feature could have been “anonymous variants”.)
    多态变体类型没有名称。 (因此此功能的另一个名称可能是“匿名变体”。)

  3. The constructors of a polymorphic variant start with a backquote character.
    多态变体的构造函数以反引号字符开头。

Using polymorphic variants, we can rewrite f:
使用多态变体,我们可以重写 f

let f = function
  | 0 -> `Infinity
  | 1 -> `Finite 1
  | n -> `Finite (-n)
val f : int -> [> `Finite of int | `Infinity ] = <fun>

This type says that f either returns `Finite n for some n : int or `Infinity. The square brackets do not denote a list, but rather a set of possible constructors. The > sign means that any code that pattern matches against a value of that type must at least handle the constructors `Finite and `Infinity, and possibly more. For example, we could write:
此类型表示 f 对于某些 n : int `Infinity 返回 `Finite n 。方括号并不表示列表,而是表示一组可能的构造函数。 > 符号意味着模式与该类型的值匹配的任何代码都必须至少处理构造函数 `Finite `Infinity ,甚至可能更多。例如,我们可以写:

match f 3 with
  | `NegInfinity -> "negative infinity"
  | `Finite n -> "finite"
  | `Infinity -> "infinite"
- : string = "finite"

It’s perfectly fine for the pattern match to include constructors other than `Finite or `Infinity, because f is guaranteed never to return any constructors other than those.
模式匹配包含 `Finite `Infinity 之外的构造函数是完全可以的,因为 f 保证永远不会返回除这些之外的任何构造函数。

There are other, more compelling uses for polymorphic variants that we’ll see later in the course. They are particularly useful in libraries. For now, we generally will steer you away from extensive use of polymorphic variants, because their types can become difficult to manage.
多态变体还有其他更引人注目的用途,我们将在课程后面看到。它们在图书馆中特别有用。目前,我们通常会引导您远离多态变体的广泛使用,因为它们的类型可能变得难以管理。

3.9.7. Built-in Variants
3.9.7. 内置变体 ¶

OCaml’s built-in list data type is really a recursive, parameterized variant. It is defined as follows:
OCaml 的内置列表数据类型实际上是一种递归的参数化变体。它的定义如下:

type 'a list = [] | ( :: ) of 'a * 'a list

So list is really just a type constructor, with (value) constructors [] (which we pronounce “nil”) and :: (which we pronounce “cons”).
所以 list 实际上只是一个类型构造函数,带有(值)构造函数 [] (我们发音为“nil”)和 :: (我们发音为“cons” )。

OCaml’s built-in option data type is also really a parameterized variant. It’s defined as follows:
OCaml 的内置选项数据类型实际上也是参数化变体。它的定义如下:

type 'a option = None | Some of 'a

So option is really just a type constructor, with (value) constructors None and Some.
所以 option 实际上只是一个类型构造函数,带有(值)构造函数 NoneSome

You can see both list and option defined in the core OCaml library.
您可以看到核心 OCaml 库中定义的 listoption

3.10. Exceptions 3.10. 状况外(异常) ¶

OCaml has an exception mechanism similar to many other programming languages. A new type of OCaml exception is defined with this syntax:
OCaml 具有与许多其他编程语言类似的异常机制。使用以下语法定义新类型的 OCaml 异常:

exception E of t

where E is a constructor name and t is a type. The of t is optional. Notice how this is similar to defining a constructor of a variant type. For example:
其中 E 是构造函数名称, t 是类型。 of t 是可选的。请注意,这与定义变体类型的构造函数有何相似之处。例如:

exception A
exception B
exception Code of int
exception Details of string
exception A
exception B
exception Code of int
exception Details of string

To create an exception value, use the same syntax you would for creating a variant value. Here, for example, is an exception value whose constructor is Failure, which carries a string:
要创建异常值,请使用与创建变量值相同的语法。例如,下面是一个异常值,其构造函数是 Failure ,它带有 string

Failure "something went wrong"
- : exn = Failure "something went wrong"

This constructor is pre-defined in the standard library and is one of the more common exceptions that OCaml programmers use.
该构造函数是在标准库中预定义的,是 OCaml 程序员使用的更常见的异常之一。

To raise an exception value e, simply write
要引发异常值 e ,只需编写

raise e

There is a convenient function failwith : string -> 'a in the standard library that raises Failure. That is, failwith s is equivalent to raise (Failure s).
标准库中有一个方便的函数 failwith : string -> 'a 可以引发 Failure 。也就是说, failwith s 相当于 raise (Failure s)

To catch an exception, use this syntax:
要捕获异常,请使用以下语法:

try e with
| p1 -> e1
| ...
| pn -> en

The expression e is what might raise an exception. If it does not, the entire try expression evaluates to whatever e does. If e does raise an exception value v, that value v is matched against the provided patterns, exactly like match expression.
表达式 e 可能会引发异常。如果不是,则整个 try 表达式的计算结果为 e 的值。如果 e 确实引发异常值 v ,则该值 v 与提供的模式匹配,与 match 表达式完全相同。

3.10.1. Exceptions are Extensible Variants
3.10.1. 异常是可扩展的不定体 ¶

All exception values have type exn, which is a variant defined in the core. It’s an unusual kind of variant, though, called an extensible variant, which allows new constructors of the variant to be defined after the variant type itself is defined. See the OCaml manual for more information about extensible variants if you’re interested.
所有异常值都具有类型 exn ,它是核心中定义的不定体。不过,这是一种不寻常的不定体,称为可扩展不定体,它允许在定义不定体类型本身之后定义不定体的新构造函数。如果您感兴趣,请参阅 OCaml 手册以获取有关可扩展不定体的更多信息。

3.10.2. Exception Semantics
3.10.2. 异常语义 ¶

Since they are just variants, the syntax and semantics of exceptions is already covered by the syntax and semantics of variants—with one exception (pun intended), which is the dynamic semantics of how exceptions are raised and handled.
由于它们只是变体,因此异常的语法和语义已经被变体的语法和语义所涵盖——只有一个异常(双关语),即如何引发和处理异常的动态语义。

Dynamic semantics. As we originally said, every OCaml expression either
动态语义。正如我们最初所说,每个 OCaml 表达式要么

  • evaluates to a value 评估为一个值

  • raises an exception 引发异常

  • or fails to terminate (i.e., an “infinite loop”).
    或无法终止(即“无限循环”)。

So far we’ve only presented the part of the dynamic semantics that handles the first of those three cases. What happens when we add exceptions? Now, evaluation of an expression either produces a value or produces an exception packet. Packets are not normal OCaml values; the only pieces of the language that recognizes them are raise and try. The exception value produced by (e.g.) Failure "oops" is part of the exception packet produced by raise (Failure "oops"), but the packet contains more than just the exception value; there can also be a stack trace, for example.
到目前为止,我们只介绍了处理这三种情况中第一种情况的动态语义部分。当我们添加异常时会发生什么?现在,表达式的计算要么产生一个值,要么产生一个异常数据包。数据包不是正常的 OCaml 值;识别它们的语言的唯一部分是 raisetry 。 (例如) Failure "oops" 生成的异常值是 raise (Failure "oops") 生成的异常数据包的一部分,但该数据包不仅仅包含异常值;例如,还可以有堆栈跟踪。

For any expression e other than try, if evaluation of a subexpression of e produces an exception packet P, then evaluation of e produces packet P.
对于除 try 之外的任何表达式 e ,如果 e 的子表达式的求值产生异常数据包 P ,则 e 生成数据包 P

But now we run into a problem for the first time: what order are subexpressions evaluated in? Sometimes the answer to that question is provided by the semantics we have already developed. For example, with let expressions, we know that the binding expression must be evaluated before the body expression. So the following code raises A:
但现在我们第一次遇到了一个问题:子表达式的求值顺序是什么?有时,这个问题的答案是由我们已经开发的语义提供的。例如,对于 let 表达式,我们知道绑定表达式必须在主体表达式之前计算。因此以下代码引发 A

let _ = raise A in raise B;;
Exception: A.
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

And with functions, OCaml does not officially specify the evaluation order of a function and its argument, but the current implementation evaluates the argument before the function. So the following code also raises A, in addition to producing some compiler warnings that the first expression will never actually be applied as a function to an argument:
对于函数,OCaml 没有正式指定函数及其参数的求值顺序,但当前的实现是在函数之前对参数求值。因此,下面的代码除了产生一些编译器警告(第一个表达式实际上永远不会作为函数应用于参数之外)还会引发 A

(raise B) (raise A)
File "[4]", line 1, characters 10-19:
1 | (raise B) (raise A)
              ^^^^^^^^^
Warning 20 [ignored-extra-argument]: this argument will not be used by the function.
File "[4]", line 1, characters 10-19:
1 | (raise B) (raise A)
              ^^^^^^^^^
Warning 20 [ignored-extra-argument]: this argument will not be used by the function.
Exception: A.
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

It makes sense that both those pieces of code would raise the same exception, given that we know let x = e1 in e2 is syntactic sugar for (fun x -> e2) e1.
鉴于我们知道 let x = e1 in e2(fun x -> e2) e1 的语法糖,这两段代码都会引发相同的异常,这是有道理的。

But what does the following code raise as an exception?
但是下面的代码会引发什么异常呢?

(raise A, raise B)
Exception: B.
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

The answer is nuanced. The language specification does not stipulate what order the components of pairs should be evaluated in. Nor did our semantics exactly determine the order. (Though you would be forgiven if you thought it was left to right.) So programmers actually cannot rely on that order. The current implementation of OCaml, as it turns out, evaluates right to left. So the code above actually raises B. If you really want to force the evaluation order, you need to use let expressions:
答案是微妙的。语言规范没有规定对的组成部分应按什么顺序进行评估。我们的语义也没有准确确定顺序。 (尽管如果您认为它是从左到右,那也情有可原。)因此,程序员实际上不能依赖该顺序。事实证明,OCaml 当前的实现是从右到左进行计算的。所以上面的代码实际上引发了 B 。如果您确实想强制执行计算顺序,则需要使用 let 表达式:

let a = raise A in
let b = raise B in
(a, b)
Exception: A.
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

That code is guaranteed to raise A rather than B.
该代码保证引发 A 而不是 B

One interesting corner case is what happens when a raise expression itself has a subexpression that raises:
一个有趣的极端情况是,当 raise 表达式本身有一个引发以下问题的子表达式时,会发生什么情况:

exception C of string;;
exception D of string;;
raise (C (raise (D "oops")))
exception C of string
exception D of string
Exception: D "oops".
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

That code ends up raising D, because the first thing that has to happen is to evaluate C (raise (D "oops")) to a value. Doing that requires evaluating raise (D "oops") to a value. Doing that causes a packet containing D "oops" to be produced, and that packet then propagates and becomes the result of evaluating C (raise (D "oops")), hence the result of evaluating raise (C (raise (D "oops"))).
该代码最终会引发 D ,因为首先要做的就是将 C (raise (D "oops")) 计算为一个值。这样做需要将 raise (D "oops") 评估为一个值。这样做会导致生成包含 D "oops" 的数据包,然后该数据包传播并成为评估 C (raise (D "oops")) 的结果,从而成为评估 raise (C (raise (D "oops"))) 的结果。

Once evaluation of an expression produces an exception packet P, that packet propagates until it reaches a try expression:
一旦表达式的计算产生异常数据包 P ,该数据包就会传播直到到达 try 表达式:

try e with
| p1 -> e1
| ...
| pn -> en

The exception value inside P is matched against the provided patterns using the usual evaluation rules for pattern matching—with one exception (again, pun intended). If none of the patterns matches, then instead of producing Match_failure inside a new exception packet, the original exception packet P continues propagating until the next try expression is reached.
P 内的异常值使用模式匹配的常用评估规则与提供的模式进行匹配,但有一个例外(再次,双关语)。如果没有任何模式匹配,则原始异常数据包 P 会继续传播,直到到达下一个 try 表达式,而不是在新的异常数据包内生成 Match_failure

3.10.3. Pattern Matching
3.10.3. 模式匹配 ¶

There is a pattern form for exceptions. Here’s an example of its usage:
有一个异常的模式形式。下面是它的用法示例:

match List.hd [] with
  | [] -> "empty"
  | _ :: _ -> "nonempty"
  | exception (Failure s) -> s
- : string = "hd"

Note that the code above is just a standard match expression, not a try expression. It matches the value of List.hd [] against the three provided patterns. As we know, List.hd [] will raise an exception containing the value Failure "hd". The exception pattern exception (Failure s) matches that value. So the above code will evaluate to "hd".
请注意,上面的代码只是一个标准的 match 表达式,而不是 try 表达式。它将 List.hd [] 的值与提供的三个模式进行匹配。众所周知, List.hd [] 将引发包含值 Failure "hd" 的异常。异常模式 exception (Failure s) 与该值匹配。所以上面的代码将计算为 "hd"

In general, exception patterns are a kind of syntactic sugar. Consider this code:
一般来说,异常模式是一种语法糖。考虑这段代码:

match e with
  | p1 -> e1
  | ...
  | pn -> en

Some of the patterns p1..pn could be exception patterns of the form exception q. Let q1..qn be that subsequence of patterns (without the exception keyword), and let r1..rm be the subsequence of non-exception patterns. Then we can rewrite the code as:
某些模式 p1..pn 可能是 exception q 形式的异常模式。令 q1..qn 为模式的子序列(不带 exception 关键字),并令 r1..rm 为非异常模式的子序列。然后我们可以将代码重写为:

try
  match e with
    | r1 -> e1
    | ...
    | rn -> en
with
  | q1 -> e1
  | ...
  | qm -> em

Which is to say: try evaluating e. If it produces an exception packet, use the exception patterns from the original match expression to handle that packet. If it doesn’t produce an exception packet but instead produces a non-exception value, use the non-exception patterns from the original match expression to match that value.
也就是说:尝试评估 e 。如果它产生异常数据包,请使用原始匹配表达式中的异常模式来处理该数据包。如果它不生成异常数据包,而是生成非异常值,则使用原始匹配表达式中的非异常模式来匹配该值。

3.10.4. Exceptions and OUnit
3.10.4. 异常和 OUnit ¶

If it is part of a function’s specification that it raises an exception, you might want to write OUnit tests that check whether the function correctly does so. Here’s how to do that:
如果引发异常是函数规范的一部分,您可能需要编写 OUnit 测试来检查函数是否正确执行此操作。具体做法如下:

open OUnit2

let tests = "suite" >::: [
    "empty" >:: (fun _ -> assert_raises (Failure "hd") (fun () -> List.hd []));
  ]

let _ = run_test_tt_main tests

The expression assert_raises exc (fun () -> e) checks to see whether expression e raises exception exc. If so, the OUnit test case succeeds, otherwise it fails.
表达式 assert_raises exc (fun () -> e) 检查表达式 e 是否引发异常 exc 。如果是,则 OUnit 测试用例成功,否则失败。

Note that the second argument of assert_raises is a function of type unit -> 'a, sometimes called a “thunk”. It may seem strange to write a function with this type—the only possible input is ()—but this is a common pattern in functional languages to suspend or delay the evaluation of a program. In this case, we want assert_raises to evaluate List.hd [] when it is ready. If we evaluated List.hd [] immediately, assert_raises would not be able to check if the right exception is raised. We’ll learn more about thunks in a later chapter.
请注意, assert_raises 的第二个参数是 unit -> 'a 类型的函数,有时称为“thunk”。用这种类型编写函数可能看起来很奇怪——唯一可能的输入是 () ——但这是函数式语言中暂停或延迟程序评估的常见模式。在本例中,我们希望 assert_raises 在准备就绪时评估 List.hd [] 。如果我们立即评估 List.hd []assert_raises 将无法检查是否引发了正确的异常。我们将在后面的章节中了解更多关于 thunk 的内容。

Warning 警告

A common error is to forget the (fun () -> ...) around e. If you make this mistake, the program may still typecheck but the OUnit test case will fail: without the extra anonymous function, the exception is raised before assert_raises ever gets a chance to handle it.
一个常见的错误是忘记 e 周围的 (fun () -> ...) 。如果您犯了这个错误,程序仍可能进行类型检查,但 OUnit 测试用例将失败:如果没有额外的匿名函数,则在 assert_raises 有机会处理它之前就会引发异常。

3.11. Example: Trees 3.11. 示例:树 ¶

Trees are a very useful data structure. A binary tree, as you’ll recall from CS 2110, is a node containing a value and two children that are trees. A binary tree can also be an empty tree, which we also use to represent the absence of a child node.
树是一种非常有用的数据结构。正如您在 CS 2110 中回忆的那样,二叉树是一个包含一个值和两个子树的节点。二叉树也可以是空树,我们也用它来表示没有子节点。

3.11.1. Representation with Tuples
3.11.1. 用元组表示 ¶

Here is a definition for a binary tree data type:
这是二叉树数据类型的定义:

type 'a tree =
| Leaf
| Node of 'a * 'a tree * 'a tree
type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree

A node carries a data item of type 'a and has a left and right subtree. A leaf is empty. Compare this definition to the definition of a list and notice how similar their structure is:
节点携带 'a 类型的数据项,并具有左子树和右子树。一片叶子是空的。将此定义与列表的定义进行比较,注意它们的结构有多么相似:

type 'a tree =                        type 'a mylist =
  | Leaf                                | Nil
  | Node of 'a * 'a tree * 'a tree      | Cons of 'a * 'a mylist

The only essential difference is that Cons carries one sublist, whereas Node carries two subtrees.
唯一本质的区别是 Cons 携带一个子列表,而 Node 携带两个子树。

Here is code that constructs a small tree:
这是构造一棵小树的代码:

(* the code below constructs this tree:
         4
       /   \
      2     5
     / \   / \
    1   3 6   7
*)
let t =
  Node(4,
    Node(2,
      Node(1, Leaf, Leaf),
      Node(3, Leaf, Leaf)
    ),
    Node(5,
      Node(6, Leaf, Leaf),
      Node(7, Leaf, Leaf)
    )
  )
val t : int tree =
  Node (4, Node (2, Node (1, Leaf, Leaf), Node (3, Leaf, Leaf)),
   Node (5, Node (6, Leaf, Leaf), Node (7, Leaf, Leaf)))

The size of a tree is the number of nodes in it (that is, Nodes, not Leafs). For example, the size of tree t above is 7. Here is a function size : 'a tree -> int that returns the number of nodes in a tree:
树的大小是其中节点的数量(即 Node ,而不是 Leaf )。例如,上面的树 t 的大小是7。下面是一个返回树中节点数的函数 size : 'a tree -> int

let rec size = function
  | Leaf -> 0
  | Node (_, l, r) -> 1 + size l + size r

3.11.2. Representation with Records
3.11.2. 用记录表示 ¶

Next, let’s revise our tree type to use use a record type to represent a tree node. In OCaml we have to define two mutually recursive types, one to represent a tree node, and one to represent a (possibly empty) tree:
接下来,让我们修改树类型以使用记录类型来表示树节点。在 OCaml 中,我们必须定义两种相互递归的类型,一种表示树节点,另一种表示一棵(可能是空的)树:

type 'a tree =
  | Leaf
  | Node of 'a node

and 'a node = {
  value: 'a;
  left: 'a tree;
  right: 'a tree
}
type 'a tree = Leaf | Node of 'a node
and 'a node = { value : 'a; left : 'a tree; right : 'a tree; }

Here’s an example tree:
这是一个示例树:

(* represents
      2
     / \
    1   3  *)
let t =
  Node {
    value = 2;
    left = Node {value = 1; left = Leaf; right = Leaf};
    right = Node {value = 3; left = Leaf; right = Leaf}
  }
val t : int tree =
  Node
   {value = 2; left = Node {value = 1; left = Leaf; right = Leaf};
    right = Node {value = 3; left = Leaf; right = Leaf}}

We can use pattern matching to write the usual algorithms for recursively traversing trees. For example, here is a recursive search over the tree:
我们可以使用模式匹配来编写递归遍历树的常用算法。例如,这是对树的递归搜索:

(** [mem x t] is whether [x] is a value at some node in tree [t]. *)
let rec mem x = function
  | Leaf -> false
  | Node {value; left; right} -> value = x || mem x left || mem x right
val mem : 'a -> 'a tree -> bool = <fun>

The function name mem is short for “member”; the standard library often uses a function of this name to implement a search through a collection data structure to determine whether some element is a member of that collection.
函数名 mem 是“member”的缩写;标准库经常使用这个名称的函数来实现对集合数据结构的搜索,以确定某个元素是否是该集合的成员。

Here’s a function that computes the preorder traversal of a tree, in which each node is visited before any of its children, by constructing a list in which the values occur in the order in which they would be visited:
这是一个计算树的前序遍历的函数,其中每个节点在其任何子节点之前被访问,通过构造一个列表,其中值按照它们被访问的顺序出现:

let rec preorder = function
  | Leaf -> []
  | Node {value; left; right} -> [value] @ preorder left @ preorder right
val preorder : 'a tree -> 'a list = <fun>
preorder t
- : int list = [2; 1; 3]

Although the algorithm is beautifully clear from the code above, it takes quadratic time on unbalanced trees because of the @ operator. That problem can be solved by introducing an extra argument acc to accumulate the values at each node, though at the expense of making the code less clear:
尽管从上面的代码来看,该算法非常清晰,但由于 @ 运算符,它在不平衡树上花费了二次方的时间。这个问题可以通过引入一个额外的参数 acc 来累积每个节点的值来解决,但代价是使代码不太清晰:

let preorder_lin t =
  let rec pre_acc acc = function
    | Leaf -> acc
    | Node {value; left; right} -> value :: (pre_acc (pre_acc acc right) left)
  in pre_acc [] t
val preorder_lin : 'a tree -> 'a list = <fun>

The version above uses exactly one :: operation per Node in the tree, making it linear time.
上面的版本对树中的每个 Node 只使用一个 :: 操作,使其成为线性时间。

3.12. Example: Natural Numbers
3.12. 示例:自然数 ¶

We can define a recursive variant that acts like numbers, demonstrating that we don’t really have to have numbers built into OCaml! (For sake of efficiency, though, it’s a good thing they are.)
我们可以定义一个像数字一样的递归变体,这表明我们实际上不必将数字内置到 OCaml 中! (不过,为了提高效率,它们是一件好事。)

A natural number is either zero or the successor of some other natural number. This is how you might define the natural numbers in a mathematical logic course, and it leads naturally to the following OCaml type nat:
自然数要么为零,要么是其他自然数的后继。这就是您在数理逻辑课程中定义自然数的方式,它自然会导致以下 OCaml 类型 nat

type nat = Zero | Succ of nat
type nat = Zero | Succ of nat

We have defined a new type nat, and Zero and Succ are constructors for values of this type. This allows us to build expressions that have an arbitrary number of nested Succ constructors. Such values act like natural numbers:
我们定义了一个新类型 nat ,并且 ZeroSucc 是该类型值的构造函数。这允许我们构建具有任意数量的嵌套 Succ 构造函数的表达式。这些值就像自然数一样:

let zero = Zero
let one = Succ zero
let two = Succ one
let three = Succ two
let four = Succ three
val zero : nat = Zero
val one : nat = Succ Zero
val two : nat = Succ (Succ Zero)
val three : nat = Succ (Succ (Succ Zero))
val four : nat = Succ (Succ (Succ (Succ Zero)))

Now we can write functions to manipulate values of this type. We’ll write a lot of type annotations in the code below to help the reader keep track of which values are nat versus int; the compiler, of course, doesn’t need our help.
现在我们可以编写函数来操作这种类型的值。我们将在下面的代码中编写大量类型注释,以帮助读者跟踪哪些值是 natint ;当然,编译器不需要我们的帮助。

let iszero = function
  | Zero -> true
  | Succ _ -> false

let pred = function
  | Zero -> failwith "pred Zero is undefined"
  | Succ m -> m
val iszero : nat -> bool = <fun>
val pred : nat -> nat = <fun>

Similarly we can define a function to add two numbers:
类似地,我们可以定义一个函数来添加两个数字:

let rec add n1 n2 =
  match n1 with
  | Zero -> n2
  | Succ pred_n -> add pred_n (Succ n2)
val add : nat -> nat -> nat = <fun>

We can convert nat values to type int and vice-versa:
我们可以将 nat 值转换为 int 类型,反之亦然:

let rec int_of_nat = function
  | Zero -> 0
  | Succ m -> 1 + int_of_nat m

let rec nat_of_int = function
  | i when i = 0 -> Zero
  | i when i > 0 -> Succ (nat_of_int (i - 1))
  | _ -> failwith "nat_of_int is undefined on negative ints"
val int_of_nat : nat -> int = <fun>
val nat_of_int : int -> nat = <fun>

To determine whether a natural number is even or odd, we can write a pair of mutually recursive functions:
为了确定自然数是偶数还是奇数,我们可以编写一对相互递归的函数:

let rec even = function Zero -> true | Succ m -> odd m
and odd = function Zero -> false | Succ m -> even m
val even : nat -> bool = <fun>
val odd : nat -> bool = <fun>

3.13. Summary 3.13. 小结 ¶

Lists are a highly useful built-in data structure in OCaml. The language provides a lightweight syntax for building them, rather than requiring you to use a library. Accessing parts of a list makes use of pattern matching, a very powerful feature (as you might expect from its rather lengthy semantics). We’ll see more uses for pattern matching as the course proceeds.
列表是 OCaml 中非常有用的内置数据结构。该语言提供了用于构建它们的轻量级语法,而不是要求您使用库。访问列表的各个部分需要使用模式匹配,这是一个非常强大的功能(正如您可能从其相当冗长的语义中所期望的那样)。随着课程的进行,我们将看到模式匹配的更多用途。

These built-in lists are implemented as singly-linked lists. That’s important to keep in mind when your needs go beyond small to medium sized lists. Recursive functions on long lists will take up a lot of stack space, so tail recursion becomes important. And if you’re attempting to process really huge lists, you probably don’t want linked lists at all, but instead a data structure that will do a better job of exploiting memory locality.
这些内置列表作为单链表实现。当您的需求超出中小型列表时,请记住这一点很重要。长列表上的递归函数会占用大量堆栈空间,因此尾递归变得很重要。如果您尝试处理非常大的列表,您可能根本不需要链表,而是需要更好地利用内存局部性的数据结构。

OCaml provides data types for variants (one-of types), tuples and products (each-of types), and options (maybe types). Pattern matching can be used to access values of each of those data types. And pattern matching can be used in let expressions and functions.
OCaml 提供变体(一种类型)、元组和乘积(每种类型)以及选项(可能是类型)的数据类型。模式匹配可用于访问每种数据类型的值。模式匹配可以用在 let 表达式和函数中。

Association lists combine lists and tuples to create a lightweight implementation of dictionaries.
关联列表结合了列表和元组来创建字典的轻量级实现。

Variants are a powerful language feature. They are the workhorse of representing data in a functional language. OCaml variants actually combine several theoretically independent language features into one: sum types, product types, recursive types, and parameterized (polymorphic) types. The result is an ability to express many kinds of data, including lists, options, trees, and even exceptions.
变体是一个强大的语言功能。它们是用函数式语言表示数据的主力。 OCaml 变体实际上将几种理论上独立的语言特性合并为一种:求和类型、乘积类型、递归类型和参数化(多态)类型。结果是能够表达多种数据,包括列表、选项、树,甚至异常。

3.13.1. Terms and Concepts
3.13.1. 术语和概念 ¶

  • algebraic data type 代数数据类型

  • append 附加

  • association list 关联列表

  • binary trees as variants 作为变体的二叉树

  • binding 捆绑

  • branch 分支

  • carried data 携带数据

  • catch-all cases 包罗万象的案例

  • cons 缺点

  • constant constructor 常量构造函数

  • constructor 构造函数

  • copying 复制

  • desugaring 脱糖

  • each-of type 每种类型

  • exception 例外

  • exception as variants 作为变体的例外

  • exception packet 异常数据包

  • exception pattern 异常模式

  • exception value 异常值

  • exhaustiveness 详尽性

  • field 场地

  • head 

  • induction 就职

  • leaf 叶子

  • list 列表

  • lists as variants 列为变体

  • maybe type 也许输入

  • mutually recursive functions
    相互递归函数

  • natural numbers as variants
    自然数的变体

  • nil

  • node 节点

  • non-constant constructor 非常量构造函数

  • one-of type 单一类型

  • options 选项

  • options as variants 选项作为变体

  • order of evaluation 评估顺序

  • pair 一对

  • parameterized variant 参数化变体

  • parametric polymorphism 参数多态性

  • pattern matching 模式匹配

  • prepend 前置

  • product type 产品类别

  • record 记录

  • recursion 递归

  • recursive variant 递归变体

  • sharing 分享

  • stack frame 栈帧

  • sum type 总和类型

  • syntactic sugar 句法糖

  • tag

  • tail 尾巴

  • tail call 尾调用

  • tail recursion 尾递归

  • test-driven development (TDD)
    测试驱动开发(TDD)

  • triple 三倍

  • tuple 元组

  • type constructor 类型构造函数

  • type synonym 类型同义词

  • variant 变体

  • wildcard 通配符

3.13.2. Further Reading
3.13.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapters 4, 5.2, 5.3, 5.4, 6, 7, 8.1
    Objective Caml 简介,第 4、5.2、5.3、5.4、6、7、8.1 章

  • OCaml from the Very Beginning, chapters 3, 4, 5, 7, 8, 10, 11
    OCaml 从头开始​​,第 3、4、5、7、8、10、11 章

  • Real World OCaml, chapter 3, 5, 6, 7
    现实世界 OCaml,第 3、5、6、7 章

3.14. Exercises 3.14. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: list expressions [★]
练习:列表表达式 [★]

  • Construct a list that has the integers 1 through 5 in it. Use the square bracket notation for lists.
    构造一个包含整数 1 到 5 的列表。对列表使用方括号表示法。

  • Construct the same list, but do not use the square bracket notation. Instead use :: and [].
    构造相同的列表,但不使用方括号表示法。而是使用 ::[]

  • Construct the same list again. This time, the following expression must appear in your answer: [2; 3; 4]. Use the @ operator, and do not use ::.
    再次构建相同的列表。这次,您的答案中必须出现以下表达式: [2; 3; 4] 。使用 @ 运算符,不要使用 ::


Exercise: product [★★] 练习:乘积[★★]

Write a function that returns the product of all the elements in a list. The product of all the elements of an empty list is 1.
编写一个函数,返回列表中所有元素的乘积。空列表的所有元素的乘积是 1


Exercise: concat [★★] 练习:字符串连接[★★]

Write a function that concatenates all the strings in a list. The concatenation of all the strings in an empty list is the empty string "".
编写一个连接列表中所有字符串的函数。空列表中所有字符串的串联是空字符串 ""


Exercise: product test [★★]
练习:乘积测试[★★]

Unit test the function product that you wrote in an exercise above.
对您在上面练习中编写的函数 product 进行单元测试。


Exercise: patterns [★★★] 练习:模式[★★★]

Using pattern matching, write three functions, one for each of the following properties. Your functions should return true if the input list has the property and false otherwise.
使用模式匹配,编写三个函数,一个函数对应以下每个属性。如果输入列表具有该属性,您的函数应返回 true ,否则返回 false

  • the list’s first element is "bigred"
    列表的第一个元素是 "bigred"

  • the list has exactly two or four elements; do not use the length function
    该列表恰好有两个或四个元素;不要使用 length 函数

  • the first two elements of the list are equal
    列表的前两个元素相等


Exercise: library [★★★] 练习:图书馆[★★★]

Consult the List standard library to solve these exercises:
请查阅 List 标准库来解决这些练习:

  • Write a function that takes an int list and returns the fifth element of that list, if such an element exists. If the list has fewer than five elements, return 0. Hint: List.length and List.nth.
    编写一个函数,它接受 int list 并返回该列表的第五个元素(如果存在)。如果列表的元素少于五个,则返回 0 。提示: List.lengthList.nth

  • Write a function that takes an int list and returns the list sorted in descending order. Hint: List.sort with Stdlib.compare as its first argument, and List.rev.
    编写一个函数,它接受 int list 并返回按降序排序的列表。提示: List.sortStdlib.compare 作为其第一个参数,以及 List.rev


Exercise: library test [★★★]
练习:库测试[★★★]

Write a couple OUnit unit tests for each of the functions you wrote in the previous exercise.
为您在上一个练习中编写的每个函数编写几个 OUnit 单元测试。


Exercise: library puzzle [★★★]
练习:图书馆拼图[★★★]

  • Write a function that returns the last element of a list. Your function may assume that the list is non-empty. Hint: Use two library functions, and do not write any pattern matching code of your own.
    编写一个返回列表最后一个元素的函数。您的函数可能假设该列表非空。提示:使用两个库函数,并且不要编写您自己的任何模式匹配代码。

  • Write a function any_zeroes : int list -> bool that returns true if and only if the input list contains at least one 0. Hint: use one library function, and do not write any pattern matching code of your own.
    编写一个函数 any_zeroes : int list -> bool ,当且仅当输入列表包含至少一个 0 时才返回 true 。提示:使用一个库函数,不要编写任何您自己的模式匹配代码。

Your solutions will be only one or two lines of code each.
您的解决方案将只有一两行代码。


Exercise: take drop [★★★]
练习:提取丢弃[★★★]

  • Write a function take : int -> 'a list -> 'a list such that take n lst returns the first n elements of lst. If lst has fewer than n elements, return all of them.
    编写一个函数 take : int -> 'a list -> 'a list ,使 take n lst 返回 lst 的第一个 n 元素。如果 lst 的元素少于 n 个元素,则返回所有元素。

  • Write a function drop : int -> 'a list -> 'a list such that drop n lst returns all but the first n elements of lst. If lst has fewer than n elements, return the empty list.
    编写一个函数 drop : int -> 'a list -> 'a list ,使 drop n lst 返回 lst 中除第一个 n 元素之外的所有元素。如果 lst 的元素少于 n 个元素,则返回空列表。


Exercise: take drop tail [★★★★]
练习:提取丢弃尾[★★★★]

Revise your solutions for take and drop to be tail recursive, if they aren’t already. Test them on long lists with large values of n to see whether they run out of stack space. To construct long lists, use the -- operator from the lists section.
takedrop 的解决方案修改为尾递归(如果还不是)。在具有较大 n 值的长列表上测试它们,以查看它们是否耗尽堆栈空间。要构造长列表,请使用列表部分中的 -- 运算符。


Exercise: unimodal [★★★] 练习:单峰[★★★]

Write a function is_unimodal : int list -> bool that takes an integer list and returns whether that list is unimodal. A unimodal list is a list that monotonically increases to some maximum value then monotonically decreases after that value. Either or both segments (increasing or decreasing) may be empty. A constant list is unimodal, as is the empty list.
编写一个函数 is_unimodal : int list -> bool ,它接受一个整数列表并返回该列表是否是单峰的。单峰列表是单调增加到某个最大值然后在该值之后单调减少的列表。其中一个或两个段(增加或减少)可以为空。常量列表是单峰的,空列表也是如此。


Exercise: powerset [★★★] 练习:powerset [★★★]

Write a function powerset : int list -> int list list that takes a set S represented as a list and returns the set of all subsets of S. The order of subsets in the powerset and the order of elements in the subsets do not matter.
编写一个函数 powerset : int list -> int list list ,它接受表示为列表的集合 S,并返回 S 的所有子集的集合。幂集中子集的顺序和子集中元素的顺序并不重要。

Hint: Consider the recursive structure of this problem. Suppose you already have p, such that p = powerset s. How could you use p to compute powerset (x :: s)?
提示:考虑这个问题的递归结构。假设您已经有 p ,例如 p = powerset s 。如何使用 p 来计算 powerset (x :: s)


Exercise: print int list rec [★★]
练习:打印整型列表递归[★★]

Write a function print_int_list : int list -> unit that prints its input list, one number per line. For example, print_int_list [1; 2; 3] should result in this output:
编写一个函数 print_int_list : int list -> unit 打印其输入列表,每行一个数字。例如, print_int_list [1; 2; 3] 应产生以下输出:

1
2
3

Here is some code to get you started:
下面是一些可以帮助您入门的代码:

let rec print_int_list = function
| [] -> ()
| h :: t -> (* fill in here *); print_int_list t

Exercise: print int list iter [★★]
练习:打印整型列表迭代[★★]

Write a function print_int_list' : int list -> unit whose specification is the same as print_int_list. Do not use the keyword rec in your solution, but instead to use the List module function List.iter. Here is some code to get you started:
编写一个函数 print_int_list' : int list -> unit ,其规格与 print_int_list 相同。不要在解决方案中使用关键字 rec ,而是使用 List 模块函数 List.iter 。下面是一些可以帮助您入门的代码:

let print_int_list' lst =
  List.iter (fun x -> (* fill in here *)) lst

Exercise: student [★★] 练习:学生[★★]

Assume the following type definition:
假设以下类型定义:

type student = {first_name : string; last_name : string; gpa : float}

Give OCaml expressions that have the following types:
给出具有以下类型的 OCaml 表达式:

  • student

  • student -> string * string (a function that extracts the student’s name)
    student -> string * string (提取学生姓名的函数)

  • string -> string -> float -> student (a function that creates a student record)
    string -> string -> float -> student (创建学生记录的函数)


Exercise: pokerecord [★★]
练习:口袋妖怪记录[★★]

Here is a variant that represents a few Pokémon types:
这是代表几种口袋妖怪(神奇宝贝)类型的变体:

type poketype = Normal | Fire | Water
  • Define the type pokemon to be a record with fields name (a string), hp (an integer), and ptype (a poketype).
    将类型 pokemon 定义为具有字段 name (字符串)、 hp (整数)和 ptype (a poketype )。

  • Create a record named charizard of type pokemon that represents a Pokémon with 78 HP and Fire type.
    创建一个名为 charizard 类型为 pokemon 的记录,代表具有 78 HP 和火属性的 Pokémon。

  • Create a record named squirtle of type pokemon that represents a Pokémon with 44 HP and Water type.
    创建一个名为 squirtle 类型 pokemon 的记录,代表具有 44 HP 和水类型的 Pokémon。


Exercise: safe hd and tl [★★]
练习:安全的头和尾[★★]

Write a function safe_hd : 'a list -> 'a option that returns Some x if the head of the input list is x, and None if the input list is empty.
编写一个函数 safe_hd : 'a list -> 'a option ,如果输入列表的头为 x ,则返回 Some x ;如果输入列表为空,则返回 None

Also write a function safe_tl : 'a list -> 'a list option that returns the tail of the list, or None if the list is empty.
还要编写一个返回列表尾部的函数 safe_tl : 'a list -> 'a list option ,如果列表为空,则返回 None


Exercise: pokefun [★★★] 练习:口袋妖怪函数[★★★]

Write a function max_hp : pokemon list -> pokemon option that, given a list of pokemon, finds the Pokémon with the highest HP.
编写一个函数 max_hp : pokemon list -> pokemon option ,根据给定的 pokemon 列表,找到 HP 最高的神奇宝贝。


Exercise: date before [★★]
练习:日期之早于[★★]

Define a date-like triple to be a value of type int * int * int. Examples of date-like triples include (2013, 2, 1) and (0, 0, 1000). A date is a date-like triple whose first part is a positive year (i.e., a year in the common era), second part is a month between 1 and 12, and third part is a day between 1 and 31 (or 30, 29, or 28, depending on the month and year). (2013, 2, 1) is a date; (0, 0, 1000) is not.
将类似日期的三元组定义为 int * int * int 类型的值。类似日期的三元组的示例包括 (2013, 2, 1)(0, 0, 1000) 。日期是一个类似日期的三元组,其第一部分是正年份(即公历中的年份),第二部分是 1 到 12 之间的月份,第三部分是 1 到 31 之间的一天(或 30, 29 或 28,具体取决于月份和年份)。 (2013, 2, 1) 是日期; (0, 0, 1000) 不是。

Write a function is_before that takes two dates as input and evaluates to true or false. It evaluates to true if the first argument is a date that comes before the second argument. (If the two dates are the same, the result is false.)
编写一个函数 is_before ,将两个日期作为输入并计算结果为 truefalse 。如果第一个参数是第二个参数之前的日期,则其计算结果为 true 。 (如果两个日期相同,则结果为 false 。)

Your function needs to work correctly only for dates, not for arbitrary date-like triples. However, you will probably find it easier to write your solution if you think about making it work for arbitrary date-like triples. For example, it’s easier to forget about whether the input is truly a date, and simply write a function that claims (for example) that January 100, 2013 comes before February 34, 2013—because any date in January comes before any date in February, but a function that says that January 100, 2013 comes after February 34, 2013 is also valid. You may ignore leap years.
您的函数需要仅对日期正确工作,而不是对任意类似日期的三元组。但是,如果您考虑使其适用于任意类似日期的三元组,您可能会发现编写解决方案更容易。例如,更容易忘记输入是否确实是日期,只需编写一个函数来声明(例如)2013 年 1 月 100 日早于 2013 年 2 月 34 日,因为 1 月的任何日期都早于 2 月的任何日期,但表示 2013 年 1 月 100 日在 2013 年 2 月 34 日之后的函数也是有效的。您可能会忽略闰年。


Exercise: earliest date [★★★]
练习:最早日期[★★★]

Write a function earliest : (int*int*int) list -> (int * int * int) option. It evaluates to None if the input list is empty, and to Some d if date d is the earliest date in the list. Hint: use is_before.
编写一个函数 earliest : (int*int*int) list -> (int * int * int) option 。如果输入列表为空,则计算结果为 None ;如果日期 d 是列表中最早的日期,则计算结果为 Some d 。提示:使用 is_before

As in the previous exercise, your function needs to work correctly only for dates, not for arbitrary date-like triples.
与上一个练习一样,您的函数只需对日期正确工作,而不对任意类似日期的三元组正确工作。


Exercise: assoc list [★] 练习:关联列表 [★]

Use the functions insert and lookup from the section on association lists to construct an association list that maps the integer 1 to the string “one”, 2 to “two”, and 3 to “three”. Lookup the key 2. Lookup the key 4.
使用关联列表部分中的函数 insertlookup 构造一个关联列表,将整数 1 映射到字符串“one”,2 映射到“two”,3 映射到字符串“one”。 “三”。查找密钥 2. 查找密钥 4.


Exercise: cards [★★] 练习:卡片[★★]

  • Define a variant type suit that represents the four suits, ♣ ♦ ♥ ♠, in a standard 52-card deck. All the constructors of your type should be constant.
    定义一个变体类型 suit ,表示标准 52 张牌中的四种花色 ♣ ♦ ♥ ♠。您的类型的所有构造函数都应该是常量。

  • Define a type rank that represents the possible ranks of a card: 2, 3, …, 10, Jack, Queen, King, or Ace. There are many possible solutions; you are free to choose whatever works for you. One is to make rank be a synonym of int, and to assume that Jack=11, Queen=12, King=13, and Ace=1 or 14. Another is to use variants.
    定义一个类型 rank 来表示一张牌的可能等级:2、3、…、10、J、Queen、K 或 Ace。有很多可能的解决方案;您可以自由选择适合您的任何内容。一种是使 rank 成为 int 的同义词,并假设 Jack=11、Queen=12、King=13 和 Ace=1 或 14。另一种是使用变种。

  • Define a type card that represents the suit and rank of a single card. Make it a record with two fields.
    定义一个类型 card 来表示单张牌的花色和点数。使其成为具有两个字段的记录。

  • Define a few values of type card: the Ace of Clubs, the Queen of Hearts, the Two of Diamonds, the Seven of Spades.
    定义一些 card 类型的值:梅花 A、红心 Q、方块 2、黑桃 7。


Exercise: matching [★] 练习:匹配[★]

For each pattern in the list below, give a value of type int option list that does not match the pattern and is not the empty list, or explain why that’s impossible.
对于下面列表中的每个模式,给出一个与模式不匹配且不是空列表的 int option list 类型值,或者解释为什么这是不可能的。

  • Some x :: tl

  • [Some 3110; None]

  • [Some x; _]

  • h1 :: h2 :: tl

  • h :: tl


Exercise: quadrant [★★] 练习:象限[★★]

Quadrant 1: x, and y both positive.  Quadrant 2: x negative, y positive.  Quadrant 3: both x and y negative.  Quadrant 4: x positive, y negative.

Complete the quadrant function below, which should return the quadrant of the given x, y point according to the diagram on the right (borrowed from Wikipedia). Points that lie on an axis do not belong to any quandrant. Hints: (a) define a helper function for the sign of an integer, (b) match against a pair.
完成下面的 quadrant 函数,该函数应根据右图(借自维基百科)返回给定 x, y 点的象限。位于轴上的点不属于任何象限。提示:(a) 为整数的符号定义一个辅助函数,(b) 与一对匹配。

type quad = I | II | III | IV
type sign = Neg | Zero | Pos

let sign (x:int) : sign =
  ...

let quadrant : int*int -> quad option = fun (x,y) ->
  match ... with
    | ... -> Some I
    | ... -> Some II
    | ... -> Some III
    | ... -> Some IV
    | ... -> None

Exercise: quadrant when [★★]
练习:象限当[★★]

Rewrite the quadrant function to use the when syntax. You won’t need your helper function from before.
重写象限函数以使用 when 语法。您将不再需要以前的辅助功能。

let quadrant_when : int*int -> quad option = function
    | ... when ... -> Some I
    | ... when ... -> Some II
    | ... when ... -> Some III
    | ... when ... -> Some IV
    | ... -> None

Exercise: depth [★★] 练习:深度[★★]

Write a function depth : 'a tree -> int that returns the number of nodes in any longest path from the root to a leaf. For example, the depth of an empty tree (simply Leaf) is 0, and the depth of tree t above is 3. Hint: there is a library function max : 'a -> 'a -> 'a that returns the maximum of any two values of the same type.
编写一个函数 depth : 'a tree -> int ,返回从根到叶的任意最长路径中的节点数。例如,空树(即 Leaf )的深度为 0 ,上面的树 t 的深度为 3 。提示:有一个库函数 max : 'a -> 'a -> 'a 返回相同类型的任意两个值中的最大值。


Exercise: shape [★★★] 练习:形状[★★★]

Write a function same_shape : 'a tree -> 'b tree -> bool that determines whether two trees have the same shape, regardless of whether the values they carry at each node are the same. Hint: use a pattern match with three branches, where the expression being matched is a pair of trees.
编写一个函数 same_shape : 'a tree -> 'b tree -> bool 来确定两棵树是否具有相同的形状,无论它们在每个节点上携带的值是否相同。提示:使用具有三个分支的模式匹配,其中匹配的表达式是一对树。


Exercise: list max exn [★★]
练习:列表最大值异常[★★]

Write a function list_max : int list -> int that returns the maximum integer in a list, or raises Failure "list_max" if the list is empty.
编写一个函数 list_max : int list -> int ,返回列表中的最大整数,或者在列表为空时引发 Failure "list_max"


Exercise: list max exn string [★★]
练习:列出最大 exn 字符串 [★★]

Write a function list_max_string : int list -> string that returns a string containing the maximum integer in a list, or the string "empty" (note, not the exception Failure "empty" but just the string "empty") if the list is empty. Hint: string_of_int in the standard library will do what its name suggests.
编写一个函数 list_max_string : int list -> string ,它返回包含列表中最大整数的字符串,或字符串 "empty" (注意,不是例外 Failure "empty" ,而只是字符串 "empty" ) 如果列表为空。提示:标准库中的 string_of_int 将执行其名称所暗示的操作。


Exercise: list max exn ounit [★]
练习:列出最大 exn ounit [★]

Write two OUnit tests to determine whether your solution to list max exn, above, correctly raises an exception when its input is the empty list, and whether it correctly returns the max value of the input list when that list is nonempty.
编写两个 OUnit 测试,以确定上面的 list max exn 解决方案在其输入为空列表时是否正确引发异常,以及在该列表非空时是否正确返回输入列表的最大值。


Exercise: is_bst [★★★★] 练习:is_bst [★★★★]

Write a function is_bst : ('a*'b) tree -> bool that returns true if and only if the given tree satisfies the binary search tree invariant. An efficient version of this function that visits each node at most once is somewhat tricky to write. Hint: write a recursive helper function that takes a tree and either gives you (i) the minimum and maximum value in the tree, or (ii) tells you that the tree is empty, or (iii) tells you that the tree does not satisfy the invariant. Your is_bst function will not be recursive, but will call your helper function and pattern match on the result. You will need to define a new variant type for the return type of your helper function.
编写一个函数 is_bst : ('a*'b) tree -> bool ,当且仅当给定的树满足二叉搜索树不变量时返回 true 。该函数的高效版本(最多访问每个节点一次)编写起来有些棘手。提示:编写一个递归辅助函数,它接受一棵树,并给出 (i) 树中的最小值和最大值,或者 (ii) 告诉你树是空的,或者 (iii) 告诉你树不存在满足不变量。您的 is_bst 函数不会递归,但会调用您的辅助函数并对结果进行模式匹配。您将需要为辅助函数的返回类型定义一个新的变体类型。


Exercise: quadrant poly [★★]
练习:象限聚[★★]

Modify your definition of quadrant to use polymorphic variants. The types of your functions should become these:
修改象限的定义以使用多态变体。您的函数类型应如下所示:

val sign : int -> [> `Neg | `Pos | `Zero ]
val quadrant : int * int -> [> `I | `II | `III | `IV ] option

4. Higher-Order Programming
4. 高阶编程 ¶

Functions are values just like any other value in OCaml. What does that mean exactly? This means that we can pass functions around as arguments to other functions, that we can store functions in data structures, that we can return functions as a result from other functions, and so forth.
函数是值,就像 OCaml 中的任何其他值一样。这到底是什么意思?这意味着我们可以将函数作为参数传递给其他函数,可以将函数存储在数据结构中,可以将函数作为其他函数的结果返回,等等。

Higher-order functions either take other functions as input or return other functions as output (or both). Higher-order functions are also known as functionals, and programming with them could therefore be called functional programming—indicating what the heart of programming in languages like OCaml is all about.
高阶函数要么采用其他函数作为输入,要么返回其他函数作为输出(或两者)。高阶函数也称为泛函,因此使用它们进行编程可以称为函数式编程,这表明 OCaml 等语言中编程的核心是什么。

Higher-order functions were one of the more recent adoptions from functional languages into mainstream languages. The Java 8 Streams library and Python 2.3’s itertools modules are examples of that; C++ has also been increasing its support since at least 2011.
高阶函数是函数式语言最近被主流语言采用的函数之一。 Java 8 Streams 库和 Python 2.3 的 itertools 模块就是这样的例子;至少自 2011 年以来,C++ 也一直在增加其支持。

Note 笔记

C wizards might object the adoption isn’t so recent. After all, C has long had the ability to do higher-order programming through function pointers. But that ability also depends on the programming pattern of passing an additional environment parameter to provide the values of variables in the function to be called through the pointer. As we’ll see in our later chapter on interpreters, the essence of (higher-order) functions in a functional language is that they are really something called a closure that obviates the need for that extra parameter. Bear in mind that the issue is not what is possible to compute in a language—after all everything is eventually compiled down to machine code, so we could just write in that exclusively—but what is pleasant to compute.
C 语言高手可能会反对,这种采用并不是最近才发生的。毕竟 C 很早就具备了通过函数指针进行高阶编程的能力。但这种能力还取决于传递附加环境参数的编程模式,以提供要通过指针调用的函数中的变量值。正如我们将在后面关于解释器的章节中看到的,函数式语言中(高阶)函数的本质是它们实际上是所谓的闭包,它消除了对额外参数的需要。请记住,问题不在于用一种语言可以计算什么——毕竟所有东西最终都会编译成机器代码,所以我们可以只用它来编写——而是计算什么是令人愉快的。

In this chapter we will see what all the fuss is about. Higher-order functions enable beautiful, general, reusable code.
在本章中,我们将看到这些大惊小怪的原因。高阶函数可以实现美观、通用、可重用的代码。

4.1. Higher-Order Functions
4.1. 高阶函数 ¶

Consider these functions double and square on integers:
考虑整数上的这些函数 doublesquare

let double x = 2 * x
let square x = x * x
val double : int -> int = <fun>
val square : int -> int = <fun>

Let’s use these functions to write other functions that quadruple and raise a number to the fourth power:
让我们使用这些函数来编写其他将数字乘四并求四次方的函数:

let quad x = double (double x)
let fourth x = square (square x)
val quad : int -> int = <fun>
val fourth : int -> int = <fun>

There is an obvious similarity between these two functions: what they do is apply a given function twice to a value. By passing in the function to another function twice as an argument, we can abstract this functionality:
这两个函数之间有一个明显的相似之处:它们所做的是将给定函数两次应用于一个值。通过将函数作为参数传递给另一个函数 twice ,我们可以抽象此功能:

let twice f x = f (f x)
val twice : ('a -> 'a) -> 'a -> 'a = <fun>

The function twice is higher-order: its input f is a function. And—recalling that all OCaml functions really take only a single argument—its output is technically fun x -> f (f x), so twice returns a function hence is also higher-order in that way.
函数 twice 是高阶的:它的输入 f 是一个函数。而且——回想一下,所有 OCaml 函数实际上只接受一个参数——从技术上讲,它的输出是 fun x -> f (f x) ,因此 twice 返回一个函数,因此也是高阶的。

Using twice, we can implement quad and fourth in a uniform way:
使用 twice ,我们可以以统一的方式实现 quadfourth

let quad x = twice double x
let fourth x = twice square x
val quad : int -> int = <fun>
val fourth : int -> int = <fun>

4.1.1. The Abstraction Principle
4.1.1. 抽象原则 ¶

Above, we have exploited the structural similarity between quad and fourth to save work. Admittedly, in this toy example it might not seem like much work. But imagine that twice were actually some much more complicated function. Then if someone comes up with a more efficient version of it, every function written in terms of it (like quad and fourth) could benefit from that improvement in efficiency, without needing to be recoded.
上面,我们利用了 quadfourth 之间的结构相似性来节省工作量。诚然,在这个玩具示例中,这似乎并不需要太多工作。但想象一下 twice 实际上是一些更复杂的函数。然后,如果有人提出了它的更高效版本,则用它编写的每个函数(例如 quadfourth )都可以从效率的提高中受益,而无需重新编码。

Part of being an excellent programmer is recognizing such similarities and abstracting them by creating functions (or other units of code) that implement them. Bruce MacLennan names this the Abstraction Principle in his textbook Functional Programming: Theory and Practice (1990). The Abstraction Principle says to avoid requiring something to be stated more than once; instead, factor out the recurring pattern. Higher-order functions enable such refactoring, because they allow us to factor out functions and parameterize functions on other functions.
成为一名优秀程序员的一部分就是认识到这些相似之处,并通过创建实现它们的函数(或其他代码单元)来抽象它们。 Bruce MacLennan 在他的教科书《函数式编程:理论与实践》(1990 年)中将其命名为“抽象原理”。抽象原则说要避免要求某件事被多次陈述;相反,排除重复出现的模式。高阶函数使这种重构成为可能,因为它们允许我们分解函数并在其他函数上参数化函数。

Besides twice, here are some more relatively simple examples, indebted also to MacLennan:
除了 twice 之外,这里还有一些相对简单的例子,也感谢 MacLennan:

Apply. We can write a function that applies its first input to its second input:
应用。我们可以编写一个函数,将其第一个输入应用到第二个输入:

let apply f x = f x
val apply : ('a -> 'b) -> 'a -> 'b = <fun>

Of course, writing apply f is a lot more work than just writing f.
当然,编写 apply f 比仅仅编写 f 需要更多的工作。

Pipeline. The pipeline operator, which we’ve previously seen, is a higher-order function:
管道。我们之前见过的管道运算符是一个高阶函数:

let pipeline x f = f x
let (|>) = pipeline
let x = 5 |> double
val pipeline : 'a -> ('a -> 'b) -> 'b = <fun>
val ( |> ) : 'a -> ('a -> 'b) -> 'b = <fun>
val x : int = 10

Compose. We can write a function that composes two other functions:
组合。我们可以编写一个由另外两个函数组成的函数:

let compose f g x = f (g x)
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

This function would let us create a new function that can be applied many times, such as the following:
这个函数可以让我们创建一个可以多次应用的新函数,如下所示:

let square_then_double = compose double square
let x = square_then_double 1
let y = square_then_double 2
val square_then_double : int -> int = <fun>
val x : int = 2
val y : int = 8

Both. We can write a function that applies two functions to the same argument and returns a pair of the result:
双应用。我们可以编写一个函数,将两个函数应用于同一个参数并返回一对结果:

let both f g x = (f x, g x)
let ds = both double square
let p = ds 3
val both : ('a -> 'b) -> ('a -> 'c) -> 'a -> 'b * 'c = <fun>
val ds : int -> int * int = <fun>
val p : int * int = (6, 9)

Cond. We can write a function that conditionally chooses which of two functions to apply based on a predicate:
条件。我们可以编写一个函数,根据谓词有条件地选择应用两个函数中的哪一个:

let cond p f g x =
  if p x then f x else g x
val cond : ('a -> bool) -> ('a -> 'b) -> ('a -> 'b) -> 'a -> 'b = <fun>

4.1.2. The Meaning of “Higher Order”
4.1.2. “高阶”的含义 ¶

The phrase “higher order” is used throughout logic and computer science, though not necessarily with a precise or consistent meaning in all cases.
“高阶”一词在逻辑和计算机科学中广泛使用,但不一定在所有情况下都具有精确或一致的含义。

In logic, first-order quantification refers primarily to the universal and existential ( and ) quantifiers. These let you quantify over some domain of interest, such as the natural numbers. But for any given quantification, say x, the variable being quantified represents an individual element of that domain, say the natural number 42.
在逻辑中,一阶量化主要指全称量词和存在量词( )。这些使您可以量化某些感兴趣的领域,例如自然数。但对于任何给定的量化,例如 x ,被量化的变量代表该域的单个元素,例如自然数 42。

Second-order quantification lets you do something strictly more powerful, which is to quantify over properties of the domain. Properties are assertions about individual elements, for example, that a natural number is even, or that it is prime. In some logics we can equate properties with sets of individual, for example the set of all even naturals. So second-order quantification is often thought of as quantification over sets. You can also think of properties as being functions that take in an element and return a Boolean indicating whether the element satisfies the property; this is called the characteristic function of the property.
二阶量化可以让您做一些更强大的事情,即对域的属性进行量化。属性是关于单个元素的断言,例如,自然数是偶数,或者是素数。在某些逻辑中,我们可以将属性等同于个体的集合,例如所有偶自然数的集合。因此二阶量化通常被认为是集合上的量化。您还可以将属性视为接受元素并返回一个布尔值的函数,该布尔值指示该元素是否满足属性;这称为属性的特征函数。

Third-order logic would allow quantification over properties of properties, and fourth-order over properties of properties of properties, and so forth. Higher-order logic refers to all these logics that are more powerful than first-order logic; though one interesting result in this area is that all higher-order logics can be expressed in second-order logic.
三阶逻辑将允许对属性的属性进行量化,四阶逻辑将允许对属性的属性的属性进行量化,等等。高阶逻辑是指所有这些比一阶逻辑更强大的逻辑;尽管这一领域的一个有趣的结果是所有高阶逻辑都可以用二阶逻辑来表达。

In programming languages, first-order functions similarly refer to functions that operate on individual data elements (e.g., strings, ints, records, variants, etc.). Whereas higher-order function can operate on functions, much like higher-order logics can quantify over over properties (which are like functions).
在编程语言中,一阶函数类似地指的是对单个数据元素(例如字符串、整数、记录、变体等)进行操作的函数。高阶函数可以对函数进行操作,就像高阶逻辑可以量化多个属性(类似于函数)一样。

4.1.3. Famous Higher-order Functions
4.1.3. 著名的高阶函数 ¶

In the next few sections we’ll dive into three of the most famous higher-order functions: map, filter, and fold. These are functions that can be defined for many data structures, including lists and trees. The basic idea of each is that:
在接下来的几节中,我们将深入研究三个最著名的高阶函数:map、filter 和 Fold。这些函数可以为许多数据结构(包括列表和树)定义。每个的基本思想是:

  • map transforms elements, 映射变换元素,

  • filter eliminates elements, and 过滤消除元素,以及

  • fold combines elements. 折叠结合元素。

4.2. Map 4.2. 映射 ¶

Here are two functions we might want to write:
以下是我们可能想要编写的两个函数:

(** [add1 lst] adds 1 to each element of [lst] *)
let rec add1 = function
  | [] -> []
  | h :: t -> (h + 1) :: add1 t

let lst1 = add1 [1; 2; 3]
val add1 : int list -> int list = <fun>
val lst1 : int list = [2; 3; 4]
(** [concat_bang lst] concatenates "!" to each element of [lst] *)
let rec concat_bang = function
  | [] -> []
  | h :: t -> (h ^ "!") :: concat_bang t

let lst2 = concat_bang ["sweet"; "salty"]
val concat_bang : string list -> string list = <fun>
val lst2 : string list = ["sweet!"; "salty!"]

There’s a lot of similarity between those two functions:
这两个函数有很多相似之处:

  • They both pattern match against a list.
    它们都针对列表进行模式匹配。

  • They both return the same value for the base case of the empty list.
    对于空列表的基本情况,它们都返回相同的值。

  • They both recurse on the tail in the case of a non-empty list.
    在非空列表的情况下,它们都在尾部递归。

In fact the only difference (other than their names) is what they do for the head element: add versus concatenate. Let’s rewrite the two functions to make that difference even more explicit:
事实上,唯一的区别(除了它们的名称)是它们对头元素的作用:添加与连接。让我们重写这两个函数,使这种差异更加明确:

(** [add1 lst] adds 1 to each element of [lst] *)
let rec add1 = function
  | [] -> []
  | h :: t ->
    let f = fun x -> x + 1 in
    f h :: add1 t

(** [concat_bang lst] concatenates "!" to each element of [lst] *)
let rec concat_bang = function
  | [] -> []
  | h :: t ->
    let f = fun x -> x ^ "!" in
    f h :: concat_bang t
val add1 : int list -> int list = <fun>
val concat_bang : string list -> string list = <fun>

Now the only difference between the two functions (again, other than their names) is the body of helper function f. Why repeat all that code when there’s such a small difference between the functions? We might as well abstract that one helper function out from each main function and make it an argument:
现在,两个函数之间的唯一区别(同样,除了它们的名称之外)是辅助函数 f 的主体。当函数之间的差异如此之小时,为什么要重复所有这些代码呢?我们不妨从每个主函数中抽象出一个辅助函数,并将其作为一个参数:

let rec add1' f = function
  | [] -> []
  | h :: t -> f h :: add1' f t

(** [add1 lst] adds 1 to each element of [lst] *)
let add1 = add1' (fun x -> x + 1)

let rec concat_bang' f = function
  | [] -> []
  | h :: t -> f h :: concat_bang' f t

(** [concat_bang lst] concatenates "!" to each element of [lst] *)
let concat_bang = concat_bang' (fun x -> x ^ "!")
val add1' : ('a -> 'b) -> 'a list -> 'b list = <fun>
val add1 : int list -> int list = <fun>
val concat_bang' : ('a -> 'b) -> 'a list -> 'b list = <fun>
val concat_bang : string list -> string list = <fun>

But now there really is no difference at all between add1' and concat_bang' except for their names. They are totally duplicated code. Even their types are now the same, because nothing about them mentions integers or strings. We might as well just keep only one of them and come up with a good new name for it. One possibility could be transform, because they transform a list by applying a function to each element of the list:
但现在,除了名称之外, add1'concat_bang' 之间实际上没有任何区别。它们是完全重复的代码。甚至它们的类型现在也是相同的,因为它们没有提到整数或字符串。我们不妨只保留其中之一,并为它起一个好听的新名字。一种可能是 transform ,因为它们通过将函数应用于列表的每个元素来转换列表:

let rec transform f = function
  | [] -> []
  | h :: t -> f h :: transform f t

(** [add1 lst] adds 1 to each element of [lst] *)
let add1 = transform (fun x -> x + 1)

(** [concat_bang lst] concatenates "!" to each element of [lst] *)
let concat_bang = transform (fun x -> x ^ "!")
val transform : ('a -> 'b) -> 'a list -> 'b list = <fun>
val add1 : int list -> int list = <fun>
val concat_bang : string list -> string list = <fun>

Note 笔记

Instead of 代替

let add1 lst = transform (fun x -> x + 1) lst

above we wrote 上面我们写了

let add1 = transform (fun x -> x + 1)

This is another way of being higher order, but it’s one we already learned about under the guise of partial application. The latter way of writing the function partially applies transform to just one of its two arguments, thus returning a function. That function is bound to the name add1.
这是实现更高阶的另一种方式,但我们已经在部分应用的幌子下了解到了这种方式。后一种编写函数的方法部分地将 transform 应用于其两个参数之一,从而返回一个函数。该函数绑定到名称 add1

Indeed, the C++ library does call the equivalent function transform. But OCaml and many other languages (including Java and Python) use the shorter word map, in the mathematical sense of how a function maps an input to an output. So let’s make one final change to that name:
事实上,C++ 库确实调用了等效函数 transform 。但 OCaml 和许多其他语言(包括 Java 和 Python)使用较短的单词映射,即函数如何将输入映射到输出的数学意义上。因此,让我们对该名称进行最后的更改:

let rec map f = function
  | [] -> []
  | h :: t -> f h :: map f t

(** [add1 lst] adds 1 to each element of [lst] *)
let add1 = map (fun x -> x + 1)

(** [concat_bang lst] concatenates "!" to each element of [lst] *)
let concat_bang = map (fun x -> x ^ "!")
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
val add1 : int list -> int list = <fun>
val concat_bang : string list -> string list = <fun>

We have now successfully applied the Abstraction Principle: the common structure has been factored out. What’s left clearly expresses the computation, at least to the reader who is familiar with map, in a way that the original versions do not as quickly make apparent.
我们现在已经成功地应用了抽象原则:共同的结构已经被分解出来。剩下的内容清楚地表达了计算,至少对于熟悉 map 的读者来说,其方式是原始版本不会很快显现出来的。

4.2.1. Side Effects 4.2.1. 副作用 ¶

The map function exists already in OCaml’s standard library as List.map, but with one small difference from the implementation we discovered above. First, let’s see what’s potentially wrong with our own implementation, then we’ll look at the standard library’s implementation.
map 函数已经作为 List.map 存在于 OCaml 的标准库中,但与我们上面发现的实现有一点细微的差别。首先,让我们看看我们自己的实现可能存在什么问题,然后我们将看看标准库的实现。

We’ve seen before in our discussion of exceptions that the OCaml language specification does not generally specify evaluation order of subexpressions, and that the current language implementation generally evaluates right-to-left. Because of that, the following (rather contrived) code actually causes the list elements to be printed in what might seem like reverse order:
我们之前在异常讨论中已经看到,OCaml 语言规范通常不指定子表达式的计算顺序,并且当前的语言实现通常从右到左计算。因此,以下(相当人为的)代码实际上会导致列表元素以看似相反的顺序打印:

let p x = print_int x; print_newline(); x + 1

let lst = map p [1; 2]
val p : int -> int = <fun>
2
1
val lst : int list = [2; 3]

Here’s why: 原因如下:

  • Expression map p [1; 2] evaluates to p 1 :: map p [2].
    表达式 map p [1; 2] 的计算结果为 p 1 :: map p [2]

  • The right-hand side of that expression is then evaluated to p 1 :: (p 2 :: map p []). The application of p to 1 has not yet occurred.
    然后将该表达式的右侧计算为 p 1 :: (p 2 :: map p [])p1 的应用尚未发生。

  • The right-hand side of :: is again evaluated next, yielding p 1 :: (p 2 :: []).
    接下来再次评估 :: 的右侧,产生 p 1 :: (p 2 :: [])

  • Then p is applied to 2, and finally to 1.
    然后 p 应用于 2 ,最后应用于 1

That is likely surprising to anyone who is predisposed to thinking that evaluation would occur left-to-right. The solution is to use a let expression to cause the evaluation of the function application to occur before the recursive call:
对于那些倾向于认为评估会从左到右进行的人来说,这可能会感到惊讶。解决方案是使用 let 表达式使函数应用程序的计算发生在递归调用之前:

let rec map f = function
  | [] -> []
  | h :: t -> let h' = f h in h' :: map f t

let lst2 = map p [1; 2]
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
1
2
val lst2 : int list = [2; 3]

Here’s why that works:
这就是为什么它有效:

  • Expression map p [1; 2] evaluates to let h' = p 1 in h' :: map p [2].
    表达式 map p [1; 2] 的计算结果为 let h' = p 1 in h' :: map p [2]

  • The binding expression p 1 is evaluated, causing 1 to be printed and h' to be bound to 2.
    评估绑定表达式 p 1 ,导致打印 1 并将 h' 绑定到 2

  • The body expression h' :: map p [2] is then evaluated, which leads to 2 being printed next.
    然后对主体表达式 h' :: map p [2] 进行求值,从而导致接下来打印 2

So that’s how the standard library defines List.map. We should use it instead of re-defining the function ourselves from now on. But it’s good that we have discovered the function “from scratch” as it were, and that if needed we could quickly re-code it.
这就是标准库定义 List.map 的方式。从现在开始我们应该使用它而不是自己重新定义函数。但好在我们“从头开始”发现了这个函数,并且如果需要的话我们可以快速重新编码。

The bigger lesson to take away from this discussion is that when evaluation order matters, we need to use let to ensure it. When does it matter? Only when there are side effects. Printing and exceptions are the two we’ve seen so far. Later we’ll add mutability.
从这次讨论中得到的更大的教训是,当评估顺序很重要时,我们需要使用 let 来确保它。什么时候重要?仅当有副作用时。到目前为止,我们已经看到了打印和异常两种情况。稍后我们将添加可变性。

4.2.2. Map and Tail Recursion
4.2.2. 映射和尾递归 ¶

Astute readers will have noticed that the implementation of map is not tail recursive. That is to some extent unavoidable. Here’s a tempting but awful way to create a tail-recursive version of it:
精明的读者会注意到 map 的实现不是尾递归的。这在某种程度上是不可避免的。这是创建尾递归版本的一种诱人但糟糕的方法:

let rec map_tr_aux f acc = function
  | [] -> acc
  | h :: t -> map_tr_aux f (acc @ [f h]) t

let map_tr f = map_tr_aux f []

let lst = map_tr (fun x -> x + 1) [1; 2; 3]
val map_tr_aux : ('a -> 'b) -> 'b list -> 'a list -> 'b list = <fun>
val map_tr : ('a -> 'b) -> 'a list -> 'b list = <fun>
val lst : int list = [2; 3; 4]

To some extent that works: the output is correct, and map_tr_aux is tail recursive. The subtle flaw is the subexpression acc @ [f h]. Recall that append is a linear-time operation on singly-linked lists. That is, if there are n list elements then append takes time O(n). So at each recursive call we perform a O(n) operation. And there will be n recursive calls, one for each element of the list. That’s a total of nO(n) work, which is O(n2). So we achieved tail recursion, but at a high cost: what ought to be a linear-time operation became quadratic time.
在某种程度上,这是有效的:输出是正确的,并且 map_tr_aux 是尾递归的。微妙的缺陷是子表达式 acc @ [f h] 。回想一下,追加是单链表上的线性时间操作。也就是说,如果有 n 列表元素,则追加需要时间 O(n) 。因此,在每次递归调用时,我们都会执行 O(n) 操作。并且将会有 n 递归调用,列表中的每个元素都会调用一次。总共有 nO(n) 工作,即 O(n2) 。因此,我们实现了尾递归,但代价很高:本来应该是线性时间的操作变成了二次时间的操作。

In an attempt to fix that, we could use the constant-time cons operation instead of the linear-time append operation:
为了解决这个问题,我们可以使用恒定时间 cons 操作而不是线性时间附加操作:

let rec map_tr_aux f acc = function
  | [] -> acc
  | h :: t -> map_tr_aux f (f h :: acc) t

let map_tr f = map_tr_aux f []

let lst = map_tr (fun x -> x + 1) [1; 2; 3]
val map_tr_aux : ('a -> 'b) -> 'b list -> 'a list -> 'b list = <fun>
val map_tr : ('a -> 'b) -> 'a list -> 'b list = <fun>
val lst : int list = [4; 3; 2]

And to some extent that works: it’s tail recursive and linear time. The not-so-subtle flaw this time is that the output is backwards. As we take each element off the front of the input list, we put it on the front of the output list, but that reverses their order.
在某种程度上,这是有效的:它是尾递归和线性时间。这次不那么微妙的缺陷是输出是向后的。当我们从输入列表的前面取出每个元素时,我们将其放在输出列表的前面,但这会颠倒它们的顺序。

Note 笔记

To understand why the reversal occurs, it might help to think of the input and output lists as people standing in a queue:
要理解为什么会发生逆转,将输入和输出列表想象成人们站在队列中可能会有所帮助:

  • Input: Alice, Bob. 输入:爱丽丝、鲍勃。

  • Output: empty. 输出:空。

Then we remove Alice from the input and add her to the output:
然后我们从输入中删除 Alice 并将她添加到输出中:

  • Input: Bob. 输入:鲍勃.

  • Output: Alice. 输出:爱丽丝。

Then we remove Bob from the input and add him to the output:
然后我们从输入中删除 Bob 并将他添加到输出中:

  • Input: empty. 输入:空。

  • Output: Bob, Alice. 输出:鲍勃,爱丽丝。

The point is that with singly-linked lists, we can only operate on the head of the list and still be constant time. We can’t move Bob to the back of the output without making him walk past Alice—and anyone else who might be standing in the output.
重点是,对于单链表,我们只能对链表的头部进行操作,而且仍然是常数时间。我们无法将鲍勃移动到输出端的后面而不让他走过爱丽丝以及可能站在输出端的任何其他人。

For that reason, the standard library calls this function List.rev_map, that is, a (tail-recursive) map function that returns its output in reverse order.
因此,标准库调用此函数 List.rev_map ,即一个(尾递归)映射函数,它以相反的顺序返回其输出。

let rec rev_map_aux f acc = function
  | [] -> acc
  | h :: t -> rev_map_aux f (f h :: acc) t

let rev_map f = rev_map_aux f []

let lst = rev_map (fun x -> x + 1) [1; 2; 3]
val rev_map_aux : ('a -> 'b) -> 'b list -> 'a list -> 'b list = <fun>
val rev_map : ('a -> 'b) -> 'a list -> 'b list = <fun>
val lst : int list = [4; 3; 2]

If you want the output in the “right” order, that’s easy: just apply List.rev to it:
如果您希望以“正确”的顺序输出,那很简单:只需对其应用 List.rev 即可:

let lst = List.rev (List.rev_map (fun x -> x + 1) [1; 2; 3])
val lst : int list = [2; 3; 4]

Since List.rev is both linear time and tail recursive, that yields a complete solution. We get a linear-time and tail-recursive map computation. The expense is that it requires two passes through the list: one to transform, the other to reverse. We’re not going to do better than this efficiency with a singly-linked list. Of course, there are other data structures that implement lists, and we’ll come to those eventually. Meanwhile, recall that we generally don’t have to worry about tail recursion (which is to say, about stack space) until lists have 10,000 or more elements.
由于 List.rev 既是线性时间又是尾递归,因此会产生完整的解决方案。我们得到了线性时间和尾递归的映射计算。代价是它需要两次遍历列表:一次进行转换,另一次进行反转。我们不会用单链表做得比这个效率更好。当然,还有其他实现列表的数据结构,我们最终会讨论这些。同时,请记住,在列表具有 10,000 个或更多元素之前,我们通常不必担心尾递归(也就是说,堆栈空间)。

Why doesn’t the standard library provide this all-in-one function? Maybe it will someday if there’s good enough reason. But you might discover in your own programming there’s not a lot of need for it. In many cases, we can either do without the tail recursion, or be content with a reversed list.
为什么标准库不提供这个一体化的功能呢?如果有足够的理由的话,也许有一天会实现。但您可能会发现在自己的编程中并没有太多需要它。在许多情况下,我们要么不使用尾递归,要么满足于反向列表。

The bigger lesson to take away from this discussion is that there can be a tradeoff between time and space efficiency for recursive functions. By attempting to make a function more space efficient (i.e., tail recursive), we can accidentally make it asympotically less time efficient (i.e., quadratic instead of linear), or if we’re clever keep the asymptotic time efficiency the same (i.e., linear) at the cost of a constant factor (i.e., processing twice).
从这次讨论中得到的更大的教训是,递归函数的时间和空间效率之间可能需要权衡。通过尝试使函数更具空间效率(即尾递归),我们可能会意外地使其渐近时间效率降低(即二次而不是线性),或者如果我们聪明的话保持渐近时间效率相同(即,线性),但代价是常数因子(即处理两次)。

4.2.3. Map in Other Languages
4.2.3. 其他语言的映射 ¶

We mentioned above that the idea of map exists in many programming languages. Here’s an example from Python:
上面我们提到,map 的概念存在于很多编程语言中。下面是一个来自 Python 的例子:

>>> print(list(map(lambda x: x + 1, [1, 2, 3])))
[2, 3, 4]

We have to use the list function to convert the result of the map back to a list, because Python for sake of efficiency produces each element of the map output as needed. Here again we see the theme of “when does it get evaluated?” returning.
我们必须使用 list 函数将 map 的结果转换回列表,因为 Python 为了提高效率会生成 map 输出的每个元素如所须。这里我们再次看到“什么时候评估?”的主题。返回。

In Java, map is part of the Stream abstraction that was added in Java 8. Since there isn’t a built-in Java syntax for lists or streams, it’s a little more verbose to give an example. Here we use a factory method Stream.of to create a stream:
在 Java 中,map 是 Java 8 中添加的 Stream 抽象的一部分。由于没有用于列表或流的内置 Java 语法,因此给出示例会更加冗长。这里我们使用工厂方法 Stream.of 来创建流:

jshell> Stream.of(1, 2, 3).map(x -> x + 1).collect(Collectors.toList())
$1 ==> [2, 3, 4]

Like in the Python example, we have to use something to convert the stream back into a list. In this case it’s the collect method.
就像在 Python 示例中一样,我们必须使用某些东西将流转换回列表。在本例中是 collect 方法。

4.3. Filter 4.3. 筛选 ¶

Suppose we wanted to filter out only the even numbers from a list, or the odd numbers. Here are some functions to do that:
假设我们只想从列表中过滤掉偶数或奇数。以下是一些可以做到这一点的函数:

(** [even n] is whether [n] is even. *)
let even n =
  n mod 2 = 0

(** [evens lst] is the sublist of [lst] containing only even numbers. *)
let rec evens = function
  | [] -> []
  | h :: t -> if even h then h :: evens t else evens t

let lst1 = evens [1; 2; 3; 4]
val even : int -> bool = <fun>
val evens : int list -> int list = <fun>
val lst1 : int list = [2; 4]
(** [odd n] is whether [n] is odd. *)
let odd n =
  n mod 2 <> 0

(** [odds lst] is the sublist of [lst] containing only odd numbers. *)
let rec odds = function
  | [] -> []
  | h :: t -> if odd h then h :: odds t else odds t

let lst2 = odds [1; 2; 3; 4]
val odd : int -> bool = <fun>
val odds : int list -> int list = <fun>
val lst2 : int list = [1; 3]

Functions evens and odds are nearly the same code: the only essential difference is the test they apply to the head element. So as we did with map in the previous section, let’s factor out that test as a function. Let’s name the function p as short for “predicate”, which is a fancy way of saying that it tests whether something is true or false:
函数 evensodds 几乎是相同的代码:唯一本质的区别是它们应用于 head 元素的测试。正如我们在上一节中对 map 所做的那样,让我们​​将该测试分解为一个函数。让我们将函数 p 命名为“predicate”的缩写,这是一种奇特的说法,它测试某件事是真是假:

let rec filter p = function
  | [] -> []
  | h :: t -> if p h then h :: filter p t else filter p t
val filter : ('a -> bool) -> 'a list -> 'a list = <fun>

And now we can reimplement our original two functions:
现在我们可以重新实现原来的两个函数:

let evens = filter even
let odds = filter odd
val evens : int list -> int list = <fun>
val odds : int list -> int list = <fun>

How simple these are! How clear! (At least to the reader who is familiar with filter.)
这些是多么简单啊!多么清楚啊! (至少对于熟悉 filter 的读者来说是这样。)

4.3.1. Filter and Tail Recursion
4.3.1. 过滤器和尾递归 ¶

As we did with map, we can create a tail-recursive version of filter:
正如我们对 map 所做的那样,我们可以创建 filter 的尾递归版本:

let rec filter_aux p acc = function
  | [] -> acc
  | h :: t -> if p h then filter_aux p (h :: acc) t else filter_aux p acc t

let filter p = filter_aux p []

let lst = filter even [1; 2; 3; 4]
val filter_aux : ('a -> bool) -> 'a list -> 'a list -> 'a list = <fun>
val filter : ('a -> bool) -> 'a list -> 'a list = <fun>
val lst : int list = [4; 2]

And again we discover the output is backwards. Here, the standard library makes a different choice than it did with map. It builds in the reversal to List.filter, which is implemented like this:
我们再次发现输出是倒退的。在这里,标准库做出了与 map 不同的选择。它建立在 List.filter 的逆转中,其实现如下:

let rec filter_aux p acc = function
  | [] -> List.rev acc (* note the built-in reversal *)
  | h :: t -> if p h then filter_aux p (h :: acc) t else filter_aux p acc t

let filter p = filter_aux p []
val filter_aux : ('a -> bool) -> 'a list -> 'a list -> 'a list = <fun>
val filter : ('a -> bool) -> 'a list -> 'a list = <fun>

Why does the standard library treat map and filter differently on this point? Good question. Perhaps there has simply never been a demand for a filter function whose time efficiency is a constant factor better. Or perhaps it is just historical accident.
为什么标准库在这一点上对 mapfilter 的处理有所不同?好问题。也许根本就没有对时间效率更好的 filter 函数的需求。或许这只是历史的偶然。

4.3.2. Filter in Other Languages
4.3.2. 过滤其他语言 ¶

Again, the idea of filter exists in many programming languages. Here it is in Python:
同样,过滤器的概念存在于许多编程语言中。这是 Python 中的:

>>> print(list(filter(lambda x: x % 2 == 0, [1, 2, 3, 4])))
[2, 4]

And in Java: 在 Java 中:

jshell> Stream.of(1, 2, 3, 4).filter(x -> x % 2 == 0).collect(Collectors.toList())
$1 ==> [2, 4]

4.4. Fold 4.4. 折叠 ¶

The map functional gives us a way to individually transform each element of a list. The filter functional gives us a way to individually decide whether to keep or throw away each element of a list. But both of those are really just looking at a single element at a time. What if we wanted to somehow combine all the elements of a list? That’s what the fold functional is for. It turns out that there are two versions of it, which we’ll study in this section. But to start, let’s look at a related function—not actually in the standard library—that we call combine.
映射函数为我们提供了一种单独转换列表中每个元素的方法。过滤函数为我们提供了一种单独决定是保留还是丢弃列表中每个元素的方法。但这两者实际上只是一次只关注一个元素。如果我们想以某种方式组合列表中的所有元素怎么办?这就是折叠功能的用途。事实证明它有两个版本,我们将在本节中研究。但首先,让我们看一个相关的函数(实际上不在标准库中),我们称之为combine。

4.4.1. Combine 4.4.1. 结合 ¶

Once more, let’s write two functions:
我们再次编写两个函数:

(** [sum lst] is the sum of all the elements of [lst]. *)
let rec sum = function
  | [] -> 0
  | h :: t -> h + sum t

let s = sum [1; 2; 3]
val sum : int list -> int = <fun>
val s : int = 6
(** [concat lst] is the concatenation of all the elements of [lst]. *)
let rec concat = function
  | [] -> ""
  | h :: t -> h ^ concat t

let c = concat ["a"; "b"; "c"]
val concat : string list -> string = <fun>
val c : string = "abc"

As when we went through similar exercises with map and filter, the functions share a great deal of common structure. The differences here are:
当我们对映射和过滤器进行类似的练习时,这些函数共享很多共同的结构。这里的区别是:

  • the case for the empty list returns a different initial value, 0 vs ""
    空列表的情况返回不同的初始值, 0""

  • the case of a non-empty list uses a different operator to combine the head element with the result of the recursive call, + vs ^.
    非空列表的情况使用不同的运算符将头元素与递归调用的结果组合起来, +^

So can we apply the Abstraction Principle again? Sure! But this time we need to factor out two arguments: one for each of those two differences.
那么我们可以再次应用抽象原则吗?当然!但这一次我们需要提出两个论点:一个论点针对这两个差异。

To start, let’s factor out only the initial value:
首先,我们只考虑初始值:

let rec sum' init = function
  | [] -> init
  | h :: t -> h + sum' init t

let sum = sum' 0

let rec concat' init = function
  | [] -> init
  | h :: t -> h ^ concat' init t

let concat = concat' ""
val sum' : int -> int list -> int = <fun>
val sum : int list -> int = <fun>
val concat' : string -> string list -> string = <fun>
val concat : string list -> string = <fun>

Now the only real difference left between sum' and concat' is the operator used to combine the head with the recursive call on the tail. That operator can also become an argument to a unified function we call combine:
现在 sum'concat' 之间唯一真正的区别是用于将头部与尾部递归调用组合起来的运算符。该运算符也可以成为我们称为 combine 的统一函数的参数:

let rec combine op init = function
  | [] -> init
  | h :: t -> op h (combine op init t)

let sum = combine ( + ) 0
let concat = combine ( ^ ) ""
val combine : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b = <fun>
val sum : int list -> int = <fun>
val concat : string list -> string = <fun>

One way to think of combine would be that:
思考 combine 的一种方法是:

  • the [] value in the list gets replaced by init, and
    列表中的 [] 值被 init 替换,并且

  • each :: constructor gets replaced by op.
    每个 :: 构造函数都被 op 替换。

For example, [a; b; c] is just syntactic sugar for a :: (b :: (c :: [])). So if we replace [] with 0 and :: with (+), we get a + (b + (c + 0)). And that would be the sum of the list.
例如, [a; b; c] 只是 a :: (b :: (c :: [])) 的语法糖。因此,如果我们将 [] 替换为 0 并将 :: 替换为 (+) ,我们会得到 a + (b + (c + 0)) 。这就是列表的总和。

Once more, the Abstraction Principle has led us to an amazingly simple and succinct expression of the computation.
再一次,抽象原理使我们得到了一种极其简单和简洁的计算表达式。

4.4.2. Fold Right 4.4.2. 右向折叠 ¶

The combine function is the idea underlying an actual OCaml library function. To get there, we need to make a couple of changes to the implementation we have so far.
combine 函数是实际 OCaml 库函数的基本思想。为了实现这一目标,我们需要对迄今为止的实现进行一些更改。

First, let’s rename some of the arguments: we’ll change op to f to emphasize that really we could pass in any function, not just a built-in operator like +. And we’ll change init to acc, which as usual stands for “accumulator”. That yields:
首先,让我们重命名一些参数:我们将 op 更改为 f 以强调我们实际上可以传入任何函数,而不仅仅是像 + 。我们将 init 更改为 acc ,它通常代表“累加器”。得出:

let rec combine f acc = function
  | [] -> acc
  | h :: t -> f h (combine f acc t)
val combine : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b = <fun>

Second, let’s make an admittedly less well-motivated change. We’ll swap the implicit list argument to combine with the init argument:
其次,我们要做出一个诚然动机不那么强烈的改变。我们将把隐式列表参数替换为 combineinit 参数:

let rec combine' f lst acc = match lst with
  | [] -> acc
  | h :: t -> f h (combine' f t acc)

let sum lst = combine' ( + ) lst 0
let concat lst = combine' ( ^ ) lst ""
val combine' : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun>
val sum : int list -> int = <fun>
val concat : string list -> string = <fun>

It’s a little less convenient to code the function this way, because we no longer get to take advantage of the function keyword, nor of partial application in defining sum and concat. But there’s no algorithmic change.
以这种方式编写函数有点不太方便,因为我们不再利用 function 关键字,也不再在定义 sumconcat

What we now have is the actual implementation of the standard library function List.fold_right. All we have left to do is change the function name:
我们现在拥有的是标准库函数 List.fold_right 的实际实现。我们剩下要做的就是更改函数名称:

let rec fold_right f lst acc = match lst with
  | [] -> acc
  | h :: t -> f h (fold_right f t acc)
val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun>

Why is this function called “fold right”? The intuition is that the way it works is to “fold in” elements of the list from the right to the left, combining each new element using the operator. For example, fold_right ( + ) [a; b; c] 0 results in evaluation of the expression a + (b + (c + 0)). The parentheses associate from the right-most subexpression to the left.
为什么这个函数叫“向右折叠”?直觉上它的工作方式是从右到左“折叠”列表的元素,使用运算符组合每个新元素。例如, fold_right ( + ) [a; b; c] 0 会导致表达式 a + (b + (c + 0)) 的计算。括号从最右边的子表达式关联到左边。

4.4.3. Tail Recursion and Combine
4.4.3. 尾递归和组合 ¶

Neither fold_right nor combine are tail recursive: after the recursive call returns, there is still work to be done in applying the function argument f or op. Let’s go back to combine and rewrite it to be tail recursive. All that requires is to change the cons branch:
fold_rightcombine 都不是尾递归:递归调用返回后,仍然需要应用函数参数 fop 并将其重写为尾递归。所需要做的就是更改 cons 分支:

let rec combine_tr f acc = function
  | [] -> acc
  | h :: t -> combine_tr f (f acc h) t  (* only real change *)
val combine_tr : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

(Careful readers will notice that the type of combine_tr is different than the type of combine. We will address that soon.)
(细心的读者会注意到 combine_tr 的类型与 combine 的类型不同。我们很快就会解决这个问题。)

Now the function f is applied to the head element h and the accumulator acc before the recursive call is made, thus ensuring there’s no work remaining to be done after the call returns. If that seems a little mysterious, here’s a rewriting of the two functions that might help:
现在,在进行递归调用之前,函数 f 被应用于头元素 h 和累加器 acc ,从而确保在递归调用之后没有剩余的工作要做。呼叫返回。如果这看起来有点神秘,下面是两个函数的重写可能会有所帮助:

let rec combine f acc = function
  | [] -> acc
  | h :: t ->
    let acc' = combine f acc t in
    f h acc'

let rec combine_tr f acc = function
  | [] -> acc
  | h :: t ->
    let acc' = f acc h in
    combine_tr f acc' t
val combine : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b = <fun>
val combine_tr : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

Pay close attention to the definition of acc', the new accumulator, in each of those version:
请密切注意每个版本中新累加器 acc' 的定义:

  • In the original version, we procrastinate using the head element h. First, we combine all the remaining tail elements to get acc'. Only then do we use f to fold in the head. So the value passed as the initial value of acc turns out to be the same for every recursive invocation of combine: it’s passed all the way down to where it’s needed, at the right-most element of the list, then used there exactly once.
    在原始版本中,我们使用头元素 h 进行拖延。首先,我们将所有剩余的尾部元素组合起来得到 acc' 。然后我们才使用 f 来折叠头部。因此,作为 acc 的初始值传递的值对于 combine 的每次递归调用都是相同的:它一直传递到需要的地方,在右侧 -列表中的大多数元素,然后只在那里使用一次。

  • But in the tail recursive version, we “pre-crastinate” by immediately folding h in with the old accumulator acc. Then we fold that in with all the tail elements. So at each recursive invocation, the value passed as the argument acc can be different.
    但在尾递归版本中,我们通过立即将 h 与旧累加器 acc 折叠在一起来“预先拖延”。然后我们将其与所有尾部元素折叠起来。因此,在每次递归调用时,作为参数 acc 传递的值可能不同。

The tail recursive version of combine works just fine for summation (and concatenation, which we elide):
组合的尾递归版本对于求和(以及我们省略的连接)来说效果很好:

let sum = combine_tr ( + ) 0
let s = sum [1; 2; 3]
val sum : int list -> int = <fun>
val s : int = 6

But something possibly surprising happens with subtraction:
但减法可能会发生一些令人惊讶的事情:

let sub = combine ( - ) 0
let s = sub [3; 2; 1]

let sub_tr = combine_tr ( - ) 0
let s' = sub_tr [3; 2; 1]
val sub : int list -> int = <fun>
val s : int = 2
val sub_tr : int list -> int = <fun>
val s' : int = -6

The two results are different!
两者的结果是不一样的!

  • With combine we compute 3 - (2 - (1 - 0)). First we fold in 1, then 2, then 3. We are processing the list from right to left, putting the initial accumulator at the far right.
    使用 combine 我们计算 3 - (2 - (1 - 0)) 。首先我们折叠 1 ,然后 2 ,然后 3 。我们从右到左处理列表,将初始累加器放在最右边。

  • But with combine_tr we compute (((0 - 3) - 2) - 1). We are processing the list from left to right, putting the initial accumulator at the far left.
    但是使用 combine_tr 我们计算 (((0 - 3) - 2) - 1) 。我们从左到右处理列表,将初始累加器放在最左边。

With addition it didn’t matter which order we processed the list, because addition is associative and commutative. But subtraction is not, so the two directions result in different answers.
对于加法,我们处理列表的顺序并不重要,因为加法是结合性和交换性的。但减法则不然,所以两个方向会得到不同的答案。

Actually this shouldn’t be too surprising if we think back to when we made map be tail recursive. Then, we discovered that tail recursion can cause us to process the list in reverse order from the non-tail recursive version of the same function. That’s what happened here.
实际上,如果我们回想一下我们何时使 map 成为尾递归,这应该不会太令人惊讶。然后,我们发现尾递归可以使我们以与同一函数的非尾递归版本相反的顺序处理列表。这就是这里发生的事情。

4.4.4. Fold Left 4.4.4. 左向折叠 ¶

Our combine_tr function is also in the standard library under the name List.fold_left:
我们的 combine_tr 函数也在标准库中,名称为 List.fold_left

let rec fold_left f acc = function
  | [] -> acc
  | h :: t -> fold_left f (f acc h) t

let sum = fold_left ( + ) 0
let concat = fold_left ( ^ ) ""
val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
val sum : int list -> int = <fun>
val concat : string list -> string = <fun>

We have once more succeeded in applying the Abstraction Principle.
我们再次成功地应用了抽象原则。

4.4.5. Fold Left vs. Fold Right
4.4.5. 左向折叠 vs 右向折叠 ¶

Let’s review the differences between fold_right and fold_left:
让我们回顾一下 fold_rightfold_left 之间的区别:

  • They combine list elements in opposite orders, as indicated by their names. Function fold_right combines from the right to the left, whereas fold_left proceeds from the left to the right.
    它们以相反的顺序组合列表元素,如其名称所示。函数 fold_right 从右到左组合,而 fold_left 从左到右组合。

  • Function fold_left is tail recursive whereas fold_right is not.
    函数 fold_left 是尾递归,而 fold_right 不是。

  • The types of the functions are different.
    函数的类型不同。

Regarding that final point, it can be hard to remember what those types are! Luckily we can always ask the toplevel:
关于最后一点,可能很难记住这些类型是什么!幸运的是,我们随时可以询问高层:

List.fold_left;;
List.fold_right;;
- : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
- : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun>

To understand those types, look for the list argument in each one of them. That tells you the type of the values in the list. Then look for the type of the return value; that tells you the type of the accumulator. From there you can work out everything else.
要了解这些类型,请在每个类型中查找列表参数。这告诉您列表中值的类型。然后寻找返回值的类型;它告诉你累加器的类型。从那里你可以解决其他一切问题。

  • In fold_left, the list argument is of type 'b list, so the list contains values of type 'b. The return type is 'a, so the accumulator has type 'a. Knowing that, we can figure out that the second argument is the initial value of the accumulator (because it has type 'a). And we can figure out that the first argument, the combining operator, takes as its own first argument an accumulator value (because it has type 'a), as its own second argument a list element (because it has type 'b), and returns a new accumulator value.
    fold_left 中,列表参数的类型为 'b list ,因此列表包含 'b 类型的值。返回类型为 'a ,因此累加器的类型为 'a 。知道了这一点,我们可以算出第二个参数是累加器的初始值(因为它的类型为 'a )。我们可以看出,第一个参数,即组合运算符,将累加器值作为其自己的第一个参数(因为它的类型为 'a ),将列表元素作为其自己的第二个参数(因为它的类型为 'b ),并返回一个新的累加器值。

  • In fold_right, the list argument is of type 'a list, so the list contains values of type 'a. The return type is 'b, so the accumulator has type 'b. Knowing that, we can figure out that the third argument is the initial value of the accumulator (because it has type 'b). And we can figure out that the first argument, the combining operator, takes as its own second argument an accumulator value (because it has type 'b), as its own first argument a list element (because it has type 'a), and returns a new accumulator value.
    fold_right 中,列表参数的类型为 'a list ,因此列表包含 'a 类型的值。返回类型为 'b ,因此累加器的类型为 'b 。知道了这一点,我们可以算出第三个参数是累加器的初始值(因为它的类型为 'b )。我们可以看出,第一个参数,即组合运算符,将累加器值作为其自己的第二个参数(因为它的类型为 'b ),将列表元素作为其自己的第一个参数(因为它的类型为 'a ),并返回一个新的累加器值。

Tip

You might wonder why the argument orders are different between the two fold functions. Good question.  好问题。 Other libraries do in fact use different argument orders. One way to remember it for OCaml is that in fold_X the accumulator argument goes to the X of the list argument.
您可能想知道为什么两个 fold 函数的参数顺序不同。好问题。其他库实际上使用不同的参数顺序。对于 OCaml 来说,记住它的一种方法是,在 fold_X 中,累加器参数转到列表参数的 X 中。

If you find it hard to keep track of all these argument orders, the ListLabels module in the standard library can help. It uses labeled arguments to give names to the combining operator (which it calls f) and the initial accumulator value (which it calls init). Internally, the implementation is actually identical to the List module.
如果您发现很难跟踪所有这些参数顺序,标准库中的 ListLabels 模块可以提供帮助。它使用带标签的参数为组合运算符(称为 f )和初始累加器值(称为 init )命名。在内部,实现实际上与 List 模块相同。

ListLabels.fold_left;;
ListLabels.fold_left ~f:(fun x y -> x - y) ~init:0 [1;2;3];;
- : f:('a -> 'b -> 'a) -> init:'a -> 'b list -> 'a = <fun>
- : int = -6
ListLabels.fold_right;;
ListLabels.fold_right ~f:(fun y x -> x - y) ~init:0 [1;2;3];;
- : f:('a -> 'b -> 'b) -> 'a list -> init:'b -> 'b = <fun>
- : int = -6

Notice how in the two applications of fold above, we are able to write the arguments in a uniform order thanks to their labels. However, we still have to be careful about which argument to the combining operator is the list element vs. the accumulator value.
请注意,在上面的两个 Fold 应用程序中,由于参数的标签,我们能够以统一的顺序编写参数。但是,我们仍然必须小心组合运算符的哪个参数是列表元素还是累加器值。

4.4.6. A Digression on Labeled Arguments and Fold
4.4.6. 关于标记参数和折叠的题外话 ¶

It’s possible to write our own version of the fold functions that would label the arguments to the combining operator, so we don’t even have to remember their order:
可以编写我们自己的折叠函数版本,将参数标记为组合运算符,因此我们甚至不必记住它们的顺序:

let rec fold_left ~op:(f: acc:'a -> elt:'b -> 'a) ~init:acc lst =
  match lst with
  | [] -> acc
  | h :: t -> fold_left ~op:f ~init:(f ~acc:acc ~elt:h) t

let rec fold_right ~op:(f: elt:'a -> acc:'b -> 'b) lst ~init:acc =
  match lst with
  | [] -> acc
  | h :: t -> f ~elt:h ~acc:(fold_right ~op:f t ~init:acc)
val fold_left : op:(acc:'a -> elt:'b -> 'a) -> init:'a -> 'b list -> 'a =
  <fun>
val fold_right : op:(elt:'a -> acc:'b -> 'b) -> 'a list -> init:'b -> 'b =
  <fun>

But those functions aren’t as useful as they might seem:
但这些功能并不像看起来那么有用:

let s = fold_left ~op:( + ) ~init:0 [1;2;3]
File "[17]", line 1, characters 22-27:
1 | let s = fold_left ~op:( + ) ~init:0 [1;2;3]
                          ^^^^^
Error: This expression has type int -> int -> int
       but an expression was expected of type acc:'a -> elt:'b -> 'a

The problem is that the built-in + operator doesn’t have labeled arguments, so we can’t pass it in as the combining operator to our labeled functions. We’d have to define our own labeled version of it:
问题是内置的 + 运算符没有标记参数,因此我们无法将其作为组合运算符传递给标记函数。我们必须定义我们自己的标记版本:

let add ~acc ~elt = acc + elt
let s = fold_left ~op:add ~init:0 [1; 2; 3]

But now we have to remember that the ~acc parameter to add will become the left-hand argument to ( + ). That’s not really much of an improvement over what we had to remember to begin with.
但现在我们必须记住 add~acc 参数将成为 ( + ) 的左侧参数。与我们一开始就必须记住的事情相比,这并没有多大的改进。

4.4.7. Using Fold to Implement Other Functions
4.4.7. 用折叠实现别的功能 ¶

Folding is so powerful that we can write many other list functions in terms of fold_left or fold_right. For example,
折叠功能非常强大,我们可以用 fold_leftfold_right 编写许多其他列表函数。例如,

let length lst =
  List.fold_left (fun acc _ -> acc + 1) 0 lst

let rev lst =
  List.fold_left (fun acc x -> x :: acc) [] lst

let map f lst =
  List.fold_right (fun x acc -> f x :: acc) lst []

let filter f lst =
  List.fold_right (fun x acc -> if f x then x :: acc else acc) lst []
val length : 'a list -> int = <fun>
val rev : 'a list -> 'a list = <fun>
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
val filter : ('a -> bool) -> 'a list -> 'a list = <fun>

At this point it begins to become debatable whether it’s better to express the computations above using folding or using the ways we have already seen. Even for an experienced functional programmer, understanding what a fold does can take longer than reading the naive recursive implementation. If you peruse the source code of the standard library, you’ll see that none of the List module internally is implemented in terms of folding, which is perhaps one comment on the readability of fold. On the other hand, using fold ensures that the programmer doesn’t accidentally program the recursive traversal incorrectly. And for a data structure that’s more complicated than lists, that robustness might be a win.
此时,使用折叠或使用我们已经看到的方式来表达上述计算是否更好,开始变得有争议。即使对于经验丰富的函数式程序员来说,理解折叠的作用也可能比阅读简单的递归实现花费更长的时间。如果你仔细阅读标准库的源代码,你会发现 List 模块内部没有一个是通过折叠实现的,这也许是对折叠可读性的一种评论。另一方面,使用fold可以确保程序员不会意外地错误地编写递归遍历。对于比列表更复杂的数据结构,这种稳健性可能是一个胜利。

4.4.8. Fold vs. Recursive vs. Library
4.4.8. 折叠 vs 递归 vs 库 ¶

We’ve now seen three different ways for writing functions that manipulate lists:
我们现在已经看到了编写操作列表的函数的三种不同方法:

  • directly as a recursive function that pattern matches against the empty list and against cons,
    直接作为递归函数,模式与空列表和缺点进行匹配,

  • using fold functions, and
    使用 fold 函数,以及

  • using other library functions.
    使用其他库函数。

Let’s try using each of those ways to solve a problem, so that we can appreciate them better.
让我们尝试使用这些方法来解决问题,以便我们可以更好地理解它们。

Consider writing a function lst_and: bool list -> bool, such that lst_and [a1; ...; an] returns whether all elements of the list are true. That is, it evaluates the same as a1 && a2 && ... && an. When applied to an empty list, it evaluates to true.
考虑编写一个函数 lst_and: bool list -> bool ,以便 lst_and [a1; ...; an] 返回列表中的所有元素是否都是 true 。也就是说,它的计算结果与 a1 && a2 && ... && an 相同。当应用于空列表时,其计算结果为 true

Here are three possible ways of writing such a function. We give each way a slightly different function name for clarity.
以下是编写此类函数的三种可能的方法。为了清楚起见,我们为每种方式提供了稍微不同的函数名称。

let rec lst_and_rec = function
  | [] -> true
  | h :: t -> h && lst_and_rec t

let lst_and_fold =
	List.fold_left (fun acc elt -> acc && elt) true

let lst_and_lib =
	List.for_all (fun x -> x)
val lst_and_rec : bool list -> bool = <fun>
val lst_and_fold : bool list -> bool = <fun>
val lst_and_lib : bool list -> bool = <fun>

The worst-case running time of all three functions is linear in the length of the list. But:
所有三个函数的最坏情况运行时间与列表的长度呈线性关系。但:

  • The first function, lst_and_rec has the advantage that it need not process the entire list. It will immediately return false the first time they discover a false element in the list.
    第一个函数 lst_and_rec 的优点是不需要处理整个列表。当他们第一次在列表中发现 false 元素时,它将立即返回 false

  • The second function, lst_and_fold, will always process every element of the list.
    第二个函数 lst_and_fold 将始终处理列表中的每个元素。

  • As for the third function lst_and_lib, according to the documentation of List.for_all, it returns (p a1) && (p a2) && ... && (p an). So like lst_and_rec it need not process every element.
    至于第三个函数 lst_and_lib ,根据 List.for_all 的文档,它返回 (p a1) && (p a2) && ... && (p an) 。因此,像 lst_and_rec 一样,它不需要处理每个元素。

4.5. Beyond Lists 4.5. 不仅仅是列表 ¶

Functionals like map and fold are not restricted to lists. They make sense for nearly any kind of data collection. For example, recall this tree representation:
像映射和折叠这样的函数并不局限于列表。它们对于几乎任何类型的数据收集都有意义。例如,回想一下这个树表示:

type 'a tree =
  | Leaf
  | Node of 'a * 'a tree * 'a tree
type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree

4.5.1. Map on Trees
4.5.1. 映射之于树 ¶

This one is easy. All we have to do is apply the function f to the value v at each node:
这个很容易。我们所要做的就是将函数 f 应用于每个节点的值 v

let rec map_tree f = function
  | Leaf -> Leaf
  | Node (v, l, r) -> Node (f v, map_tree f l, map_tree f r)
val map_tree : ('a -> 'b) -> 'a tree -> 'b tree = <fun>

4.5.2. Fold on Trees
4.5.2. 折叠之于树 ¶

This one is only a little harder. Let’s develop a fold functional for 'a tree similar to our fold_right over 'a list. One way to think of List.fold_right would be that the [] value in the list gets replaced by the acc argument, and each :: constructor gets replaced by an application of the f argument. For example, [a; b; c] is syntactic sugar for a :: (b :: (c :: [])). So if we replace [] with 0 and :: with ( + ), we get a + (b + (c + 0)). Along those lines, here’s a way we could rewrite fold_right that will help us think a little more clearly:
这个只是稍微难一点。让我们为 'a tree 开发一个折叠函数,类似于 fold_right over 'a list 。考虑 List.fold_right 的一种方法是列表中的 [] 值被 acc 参数替换,并且每个 :: 构造函数被 f 参数的应用替换。例如, [a; b; c]a :: (b :: (c :: [])) 的语法糖。因此,如果我们将 [] 替换为 0 并将 :: 替换为 ( + ) ,我们会得到 a + (b + (c + 0)) 。沿着这些思路,我们可以重写 fold_right ,这将帮助我们更清晰地思考:

type 'a mylist =
  | Nil
  | Cons of 'a * 'a mylist

let rec fold_mylist f acc = function
  | Nil -> acc
  | Cons (h, t) -> f h (fold_mylist f acc t)
type 'a mylist = Nil | Cons of 'a * 'a mylist
val fold_mylist : ('a -> 'b -> 'b) -> 'b -> 'a mylist -> 'b = <fun>

The algorithm is the same. All we’ve done is to change the definition of lists to use constructors written with alphabetic characters instead of punctuation, and to change the argument order of the fold function.
算法是一样的。我们所做的只是更改列表的定义,以使用用字母字符而不是标点符号编写的构造函数,并更改折叠函数的参数顺序。

For trees, we’ll want the initial value of acc to replace each Leaf constructor, just like it replaced [] in lists. And we’ll want each Node constructor to be replaced by the operator. But now the operator will need to be ternary instead of binary—that is, it will need to take three arguments instead of two—because a tree node has a value, a left child, and a right child, whereas a list cons had only a head and a tail.
对于树,我们希望 acc 的初始值替换每个 Leaf 构造函数,就像它替换列表中的 [] 一样。我们希望每个 Node 构造函数都被运算符替换。但现在操作符需要是三元的而不是二元的——也就是说,它需要接受三个参数而不是两个——因为树节点有一个值、一个左子节点和一个右子节点,而列表 cons 只有一个头和一个尾。

Inspired by those observations, here is the fold function on trees:
受这些观察的启发,这是树上的折叠函数:

let rec fold_tree f acc = function
  | Leaf -> acc
  | Node (v, l, r) -> f v (fold_tree f acc l) (fold_tree f acc r)
val fold_tree : ('a -> 'b -> 'b -> 'b) -> 'b -> 'a tree -> 'b = <fun>

If you compare that function to fold_mylist, you’ll note it very nearly identical. There’s just one more recursive call in the second pattern-matching branch, corresponding to the one more occurrence of 'a tree in the definition of that type.
如果将该函数与 fold_mylist 进行比较,您会发现它几乎完全相同。在第二个模式匹配分支中只有一次递归调用,对应于该类型定义中又出现一次 'a tree

We can then use fold_tree to implement some of the tree functions we’ve previously seen:
然后我们可以使用 fold_tree 来实现我们之前见过的一些树函数:

let size t = fold_tree (fun _ l r -> 1 + l + r) 0 t
let depth t = fold_tree (fun _ l r -> 1 + max l r) 0 t
let preorder t = fold_tree (fun x l r -> [x] @ l @ r) [] t
val size : 'a tree -> int = <fun>
val depth : 'a tree -> int = <fun>
val preorder : 'a tree -> 'a list = <fun>

Why did we pick fold_right and not fold_left for this development? Because fold_left is tail recursive, which is something we’re never going to achieve on binary trees. Suppose we process the left branch first; then we still have to process the right branch before we can return. So there will always be work left to do after a recursive call on one branch. Thus on trees an equivalent to fold_right is the best which we can hope for.
为什么我们选择 fold_right 而不是 fold_left 来进行此开发?因为 fold_left 是尾递归的,这是我们在二叉树上永远无法实现的。假设我们先处理左分支;那么我们仍然需要处理正确的分支才能返回。因此,在一个分支上进行递归调用后,总会有一些工作要做。因此,在树上,相当于 fold_right 是我们所希望的最好结果。

The technique we used to derive fold_tree works for any OCaml variant type t:
我们用来派生 fold_tree 的技术适用于任何 OCaml 变体类型 t

  • Write a recursive fold function that takes in one argument for each constructor of t.
    编写一个递归 fold 函数,该函数为 t 的每个构造函数接收一个参数。

  • That fold function matches against the constructors, calling itself recursively on any value of type t that it encounters.
    fold 函数与构造函数匹配,在遇到的 t 类型的任何值上递归调用自身。

  • Use the appropriate argument of fold to combine the results of all recursive calls as well as all data not of type t at each constructor.
    使用 fold 的适当参数来组合每个构造函数中所有递归调用的结果以及所有非 t 类型的数据。

This technique constructs something called a catamorphism, aka a generalized fold operation. To learn more about catamorphisms, take a course on category theory.
这种技术构建了一种称为变形的东西,也称为广义折叠操作。要了解有关变形论的更多信息,请学习范畴论课程。

4.5.3. Filter on Trees
4.5.3. 过滤之于树 ¶

This one is perhaps the hardest to design. The problem is: if we decide to filter a node, what should we do with its children?
这可能是最难设计的。问题是:如果我们决定过滤一个节点,我们应该如何处理它的子节点?

  • We could recurse on the children. If after filtering them only one child remains, we could promote it in place of its parent. But what if both children remain, or neither? Then we’d somehow have to reshape the tree. Without knowing more about how the tree is intended to be used—that is, what kind of data it represents—we are stuck.
    我们可以对孩子们进行递归。如果过滤后只剩下一个孩子,我们可以将其提升以代替其父母。但如果两个孩子都留下来怎么办?然后我们就必须以某种方式重塑这棵树。如果不了解更多有关如何使用树的信息(即它代表什么类型的数据),我们就会陷入困境。

  • Instead, we could just eliminate the children entirely. So the decision to filter a node means pruning the entire subtree rooted at that node.
    相反,我们可以完全消除孩子们。因此,过滤节点的决定意味着修剪以该节点为根的整个子树。

The latter is easy to implement:
后者很容易实现:

let rec filter_tree p = function
  | Leaf -> Leaf
  | Node (v, l, r) ->
    if p v then Node (v, filter_tree p l, filter_tree p r) else Leaf
val filter_tree : ('a -> bool) -> 'a tree -> 'a tree = <fun>

4.6. Pipelining 4.6. 流水线(管道/管线) ¶

Suppose we wanted to compute the sum of squares of the numbers from 0 up to n. How might we go about it? Of course (math being the best form of optimization), the most efficient way would be a closed-form formula:
假设我们要计算从 0 到 n 的数字的平方和。我们可以怎样做呢?当然(数学是最好的优化形式),最有效的方法是封闭式公式:

n(n+1)(2n+1)6

But let’s imagine you’ve forgotten that formula. In an imperative language you might use a for loop:
但假设你已经忘记了这个公式。在命令式语言中,您可以使用 for 循环:

# Python
def sum_sq(n):
	sum = 0
	for i in range(0, n+1):
		sum += i * i
	return sum

The equivalent (tail) recursive code in OCaml would be:
OCaml 中的等效(尾)递归代码为:

let sum_sq n =
  let rec loop i sum =
    if i > n then sum
    else loop (i + 1) (sum + i * i)
  in loop 0 0
val sum_sq : int -> int = <fun>

Another, clearer way of producing the same result in OCaml uses higher-order functions and the pipeline operator:
在 OCaml 中产生相同结果的另一种更清晰的方法是使用高阶函数和管道运算符:

let rec ( -- ) i j = if i > j then [] else i :: i + 1 -- j
let square x = x * x
let sum = List.fold_left ( + ) 0

let sum_sq n =
  0 -- n              (* [0;1;2;...;n]   *)
  |> List.map square  (* [0;1;4;...;n*n] *)
  |> sum              (*  0+1+4+...+n*n  *)
val ( -- ) : int -> int -> int list = <fun>
val square : int -> int = <fun>
val sum : int list -> int = <fun>
val sum_sq : int -> int = <fun>

The function sum_sq first constructs a list containing all the numbers 0..n. Then it uses the pipeline operator |> to pass that list through List.map square, which squares every element. Then the resulting list is pipelined through sum, which adds all the elements together.
函数 sum_sq 首先构造一个包含所有数字 0..n 的列表。然后它使用管道运算符 |> 将该列表传递给 List.map square ,该列表对每个元素进行平方。然后,结果列表通过 sum 进行管道传输,将所有元素添加在一起。

The other alternatives that you might consider are somewhat uglier:
您可能会考虑的其他替代方案有些丑陋:

(* Maybe worse: a lot of extra [let..in] syntax and unnecessary names to
   for intermediate values we don't care about. *)
let sum_sq n =
  let l = 0 -- n in
  let sq_l = List.map square l in
  sum sq_l

(* Maybe worse: have to read the function applications from right to left
   rather than top to bottom, and extra parentheses. *)
let sum_sq n =
  sum (List.map square (0--n))
val sum_sq : int -> int = <fun>
val sum_sq : int -> int = <fun>

The downside of all of these compared to the original tail recursive version is that they are wasteful of space—linear instead of constant—and take a constant factor more time. So as is so often the case in programming, there is a tradeoff between clarity and efficiency of code.
与原始尾递归版本相比,所有这些的缺点是它们浪费空间(线性而不是常数)并且花费常数因子更多的时间。正如编程中经常出现的情况一样,在代码的清晰度和效率之间需要进行权衡。

Note that the inefficiency is not from the pipeline operator itself, but from having to construct all those unnecessary intermediate lists. So don’t get the idea that pipelining is intrinsically bad. In fact it can be quite useful. When we get to the chapter on modules, we’ll use it quite often with some of the data structures we study there.
请注意,低效率不是来自管道运算符本身,而是来自必须构造所有那些不必要的中间列表。因此,不要认为管道本质上是不好的。事实上它非常有用。当我们进入模块这一章时,我们会经常将它与我们在那里学习的一些数据结构一起使用。

4.7. Currying 4.7. 柯里化 ¶

We’ve already seen that an OCaml function that takes two arguments of types t1 and t2 and returns a value of type t3 has the type t1 -> t2 -> t3. We use two variables after the function name in the let expression:
我们已经看到,一个 OCaml 函数采用 t1t2 类型的两个参数并返回 t3 类型的值,其类型为 t1 -> t2 -> t3

let add x y = x + y
val add : int -> int -> int = <fun>

Another way to define a function that takes two arguments is to write a function that takes a tuple:
定义带有两个参数的函数的另一种方法是编写一个带有元组的函数:

let add' t = fst t + snd t
val add' : int * int -> int = <fun>

Instead of using fst and snd, we could use a tuple pattern in the definition of the function, leading to a third implementation:
我们可以在函数的定义中使用元组模式,而不是使用 fstsnd ,从而实现第三种实现:

let add'' (x, y) = x + y
val add'' : int * int -> int = <fun>

Functions written using the first style (with type t1 -> t2 -> t3) are called curried functions, and functions using the second style (with type t1 * t2 -> t3) are called uncurried. Metaphorically, curried functions are “spicier” because you can partially apply them (something you can’t do with uncurried functions: you can’t pass in half of a pair). Actually, the term curry does not refer to spices, but to a logician named Haskell Curry (one of a very small set of people with programming languages named after both their first and last names).
(是极少数名字和姓氏都被用于命名的编程语言的人之一)

使用第一种风格(类型为 t1 -> t2 -> t3 )编写的函数称为柯里化函数,使用第二种风格(类型为 t1 * t2 -> t3 )编写的函数称为非柯里化函数。打个比方,柯里化函数“更辣”,因为你可以部分应用它们(这是非柯里化函数无法做到的:你不能传入一半的对)。实际上,“咖喱”一词并不是指香料,而是指一位名叫 Haskell Curry 的逻辑学家(是极少数名字和姓氏都被用于命名的编程语言的人之一)。

Sometimes you will come across libraries that offer an uncurried version of a function, but you want a curried version of it to use in your own code; or vice versa. So it is useful to know how to convert between the two kinds of functions, as we did with add above.
有时,您会遇到提供函数的非柯里化版本的库,但您希望在自己的代码中使用该函数的柯里化版本;或相反亦然。因此,了解如何在两种函数之间进行转换是很有用的,就像我们在上面的 add 中所做的那样。

You could even write a couple of higher-order functions to do the conversion for you:
您甚至可以编写几个高阶函数来为您进行转换:

let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
val curry : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
val uncurry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun>
let uncurried_add = uncurry add
let curried_add = curry add''
val uncurried_add : int * int -> int = <fun>
val curried_add : int -> int -> int = <fun>

4.8. Summary 4.8. 小结 ¶

This chapter is one of the most important in the book. It didn’t cover any new language features. Instead, we learned how to use some of the existing features in ways that might be new, surprising, or challenging. Higher-order programming and the Abstraction Principle are two ideas that will help make you a better programmer in any language, not just OCaml. Of course, languages do vary in the extent to which they support these ideas, with some providing significantly less assistance in writing higher-order code—which is one reason we use OCaml in this course.
本章是本书中最重要的章节之一。它没有涵盖任何新的语言功能。相反,我们学习了如何以可能是新的、令人惊讶的或具有挑战性的方式使用一些现有功能。高阶编程和抽象原则这两个想法将帮助您成为任何语言中更好的程序员,而不仅仅是 OCaml。当然,不同语言对这些想法的支持程度确实有所不同,有些语言在编写高阶代码方面提供的帮助明显较少,这也是我们在本课程中使用 OCaml 的原因之一。

Map, filter, fold and other functionals are becoming widely recognized as excellent ways to structure computation. Part of the reason for that is they factor out the iteration over a data structure from the computation done at each element. Languages such as Python, Ruby, and Java 8 now have support for this kind of iteration.
映射、过滤器、折叠和其他函数被广泛认为是构建计算的优秀方法。部分原因是他们从每个元素完成的计算中提取出数据结构的迭代。 Python、Ruby 和 Java 8 等语言现在都支持这种迭代。

4.8.1. Terms and Concepts
4.8.1. 术语和概念 ¶

  • Abstraction Principle 抽象原则

  • accumulator 累加器

  • apply 申请

  • associative 联想性的

  • compose 撰写

  • factor 因素

  • filter 筛选

  • first-order function 一阶函数

  • fold 折叠

  • functional 功能性的

  • generalized fold operation
    广义折叠操作

  • higher-order function 高阶函数

  • map

  • pipeline 管道

  • pipelining 流水线

4.8.2. Further Reading 4.8.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapters 3.1.3, 5.3
    Objective Caml 简介,第 3.1.3、5.3 章

  • OCaml from the Very Beginning, chapter 6
    OCaml 从头开始​​,第 6 章

  • More OCaml: Algorithms, Methods, and Diversions, chapter 1, by John Whitington. This book is a sequel to OCaml from the Very Beginning.
    更多 OCaml:算法、方法和转移,第 1 章,作者:John Whitington。本书是《OCaml from the Very Beginning》的续集。

  • Real World OCaml, chapter 3 (beware that this book’s Core library has a different List module than the standard library’s List module, with different types for map and fold than those we saw here)
    现实世界 OCaml,第 3 章(请注意,本书的 Core 库具有与标准库的 List 模块不同的 List 模块, mapfold 比我们在这里看到的那些)

  • “Higher Order Functions”, chapter 6 of Functional Programming: Practice and Theory. Bruce J. MacLennan, Addison-Wesley, 1990. Our discussion of higher-order functions and the Abstraction Principle is indebted to this chapter.
    “高阶函数”,《函数式编程:实践与理论》第 6 章。 Bruce J. MacLennan,Addison-Wesley,1990。我们对高阶函数和抽象原理的讨论得益于本章。

  • “Can Programming be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs.” John Backus’ 1977 Turing Award lecture in its elaborated form as a published article.
    《编程能否从冯诺依曼风格中解放出来?函数式风格及其程序代数。”约翰·巴克斯 (John Backus) 1977 年图灵奖演讲的详细形式为已发表的文章。

  • Second-order and Higher-order Logic” in The Stanford Encyclopedia of Philosophy.
    斯坦福哲学百科全书中的“二阶和高阶逻辑”。

4.9. Exercises 4.9. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: twice, no arguments [★]
练习:两次, 无参数[★]

Consider the following definitions:
考虑以下定义:

let double x = 2*x
let square x = x*x
let twice f x = f (f x)
let quad = twice double
let fourth = twice square

Use the toplevel to determine what the types of quad and fourth are. Explain how it can be that quad is not syntactically written as a function that takes an argument, and yet its type shows that it is in fact a function.
使用顶层来确定 quadfourth 的类型。解释为什么 quad 在语法上没有写成带有参数的函数,但它的类型表明它实际上是一个函数。


Exercise: mystery operator 1 [★★]
练习:神秘算子1 [★★]

What does the following operator do?
下面的运算符是做什么的?

let ( $ ) f x = f x

Hint: investigate square $ 2 + 2 vs. square 2 + 2.
提示:调查 square $ 2 + 2square 2 + 2


Exercise: mystery operator 2 [★★]
练习:神秘算子2 [★★]

What does the following operator do?
下面的运算符是做什么的?

let ( @@ ) f g x = x |> g |> f

Hint: investigate String.length @@ string_of_int applied to 1, 10, 100, etc.
提示:调查 String.length @@ string_of_int 应用于 110100 等。


Exercise: repeat [★★] 练习:重复[★★]

Generalize twice to a function repeat, such that repeat f n x applies f to x a total of n times. That is,
twice 推广到函数 repeat ,这样 repeat f n xf 应用于 x 总共 n 次。那是,

  • repeat f 0 x yields x
    repeat f 0 x 产生 x

  • repeat f 1 x yields f x
    repeat f 1 x 产生 f x

  • repeat f 2 x yields f (f x) (which is the same as twice f x)
    repeat f 2 x 产生 f (f x) (与 twice f x 相同)

  • repeat f 3 x yields f (f (f x))
    repeat f 3 x 产生 f (f (f x))


Exercise: product [★] 练习:乘积[★]

Use fold_left to write a function product_left that computes the product of a list of floats. The product of the empty list is 1.0. Hint: recall how we implemented sum in just one line of code in lecture.
使用 fold_left 编写一个函数 product_left 来计算浮点数列表的乘积。空列表的乘积是 1.0 。提示:回想一下我们在讲座中如何仅用一行代码实现 sum

Use fold_right to write a function product_right that computes the product of a list of floats. Same hint applies.
使用 fold_right 编写一个函数 product_right 来计算浮点数列表的乘积。同样的提示也适用。


Exercise: terse product [★★]
练习:简洁的乘积[★★]

How terse can you make your solutions to the product exercise? Hints: you need only one line of code for each, and you do not need the fun keyword. For fold_left, your function definition does not even need to explicitly take a list argument. If you use ListLabels, the same is true for fold_right.
您能将乘积练习的解决方案简洁到什么程度?提示:每个只需要一行代码,并且不需要 fun 关键字。对于 fold_left ,您的函数定义甚至不需要显式采用列表参数。如果您使用 ListLabels ,则 fold_right 也是如此。


Exercise: sum_cube_odd [★★]
练习:奇数立方和[★★]

Write a function sum_cube_odd n that computes the sum of the cubes of all the odd numbers between 0 and n inclusive. Do not write any new recursive functions. Instead, use the functionals map, fold, and filter, and the ( -- ) operator (defined in the discussion of pipelining).
编写一个函数 sum_cube_odd n ,计算 0n 之间所有奇数的立方和(含)。不要编写任何新的递归函数。相反,请使用函数映射、折叠和过滤器以及 ( -- ) 运算符(在流水线讨论中定义)。


Exercise: sum_cube_odd pipeline [★★]
练习:管道式奇数立方和[★★]

Rewrite the function sum_cube_odd to use the pipeline operator |>.
重写函数 sum_cube_odd 以使用管道运算符 |>


Exercise: exists [★★] 练习:存在[★★]

Consider writing a function exists: ('a -> bool) -> 'a list -> bool, such that exists p [a1; ...; an] returns whether at least one element of the list satisfies the predicate p. That is, it evaluates the same as (p a1) || (p a2) || ... || (p an). When applied to an empty list, it evaluates to false.
考虑编写一个函数 exists: ('a -> bool) -> 'a list -> bool ,以便 exists p [a1; ...; an] 返回列表中的至少一个元素是否满足谓词 p 。也就是说,它的计算结果与 (p a1) || (p a2) || ... || (p an) 相同。当应用于空列表时,其计算结果为 false

Write three solutions to this problem, as we did above:
正如我们上面所做的那样,写出这个问题的三个解决方案:

  • exists_rec, which must be a recursive function that does not use the List module,
    exists_rec ,它必须是不使用 List 模块的递归函数,

  • exists_fold, which uses either List.fold_left or List.fold_right, but not any other List module functions nor the rec keyword, and
    exists_fold ,它使用 List.fold_leftList.fold_right ,但不使用任何其他 List 模块函数或 rec 关键字, 和

  • exists_lib, which uses any combination of List module functions other than fold_left or fold_right, and does not use the rec keyword.
    exists_lib ,它使用 fold_leftfold_right 之外的 List 模块函数的任意组合,并且不使用 rec


Exercise: account balance [★★★]
练习:账户余额[★★★]

Write a function which, given a list of numbers representing debits, deducts them from an account balance, and finally returns the remaining amount in the balance. Write three versions: fold_left, fold_right, and a direct recursive implementation.
编写一个函数,给定代表借方的数字列表,从帐户余额中扣除它们,最后返回余额中的剩余金额。编写三个版本: fold_leftfold_right 和直接递归实现。


Exercise: library uncurried [★★]
练习:非柯里化库[★★]

Here is an uncurried version of List.nth:
这是 List.nth 的非柯里化版本:

let uncurried_nth (lst, n) = List.nth lst n

In a similar way, write uncurried versions of these library functions:
以类似的方式编写这些库函数的非柯里化版本:

  • List.append

  • Char.compare

  • Stdlib.max


Exercise: map composition [★★★]
练习:组合映射[★★★]

Show how to replace any expression of the form List.map f (List.map g lst) with an equivalent expression that calls List.map only once.
演示如何将 List.map f (List.map g lst) 形式的任何表达式替换为仅调用 List.map 一次的等效表达式。


Exercise: more list fun [★★★]
练习:更多列表函数[★★★]

Write functions that perform the following computations. Each function that you write should use one of List.fold, List.map or List.filter. To choose which of those to use, think about what the computation is doing: combining, transforming, or filtering elements.
编写执行以下计算的函数。您编写的每个函数都应使用 List.foldList.mapList.filter 之一。要选择使用其中哪一个,请考虑计算正在执行的操作:组合、转换或过滤元素。

  • Find those elements of a list of strings whose length is strictly greater than 3.
    查找长度严格大于 3 的字符串列表中的那些元素。

  • Add 1.0 to every element of a list of floats.
    1.0 添加到浮点数列表的每个元素。

  • Given a list of strings strs and another string sep, produce the string that contains every element of strs separated by sep. For example, given inputs ["hi";"bye"] and ",", produce "hi,bye", being sure not to produce an extra comma either at the beginning or end of the result string.
    给定一个字符串列表 strs 和另一个字符串 sep ,生成包含 strs 中由 sep 分隔的每个元素的字符串。例如,给定输入 ["hi";"bye"]"," ,生成 "hi,bye" ,确保不会在结果字符串的开头或结尾生成额外的逗号。


Exercise: association list keys [★★★]
练习:关联列表键[★★★]

Recall that an association list is an implementation of a dictionary in terms of a list of pairs, in which we treat the first component of each pair as a key and the second component as a value.
回想一下,关联列表是字典的对列表的实现,其中我们将每对的第一个组件视为键,将第二个组件视为值。

Write a function keys: ('a * 'b) list -> 'a list that returns a list of the unique keys in an association list. Since they must be unique, no value should appear more than once in the output list. The order of values output does not matter. How compact and efficient can you make your solution? Can you do it in one line and linearithmic space and time? Hint: List.sort_uniq.
编写一个函数 keys: ('a * 'b) list -> 'a list ,返回关联列表中唯一键的列表。由于它们必须是唯一的,因此任何值都不应在输出列表中出现多次。值输出的顺序并不重要。您的解决方案能够达到多么紧凑和高效的程度?你能用一条线和线性空间和时间来完成吗?提示: List.sort_uniq


Exercise: valid matrix [★★★]
练习:有效矩阵[★★★]

A mathematical matrix can be represented with lists. In row-major representation, this matrix
数学矩阵可以用列表来表示。在行优先表示中,该矩阵

[111987]

would be represented as the list [[1; 1; 1]; [9; 8; 7]]. Let’s represent a row vector as an int list. For example, [9; 8; 7] is a row vector.
将表示为列表 [[1; 1; 1]; [9; 8; 7]] 。让我们将行向量表示为 int list 。例如, [9; 8; 7] 是行向量。

A valid matrix is an int list list that has at least one row, at least one column, and in which every column has the same number of rows. There are many values of type int list list that are invalid, for example,
有效矩阵是一个 int list list ,它至少具有一行、至少一列,并且其中每列具有相同的行数。 int list list 类型的许多值都是无效的,例如,

  • []

  • [[1; 2]; [3]]

Implement a function is_valid_matrix: int list list -> bool that returns whether the input matrix is valid. Unit test the function.
实现一个函数 is_valid_matrix: int list list -> bool 返回输入矩阵是否有效。对功能进行单元测试。


Exercise: row vector add [★★★]
练习:行向量加法[★★★]

Implement a function add_row_vectors: int list -> int list -> int list for the element-wise addition of two row vectors. For example, the addition of [1; 1; 1] and [9; 8; 7] is [10; 9; 8]. If the two vectors do not have the same number of entries, the behavior of your function is unspecified—that is, it may do whatever you like. Hint: there is an elegant one-line solution using List.map2. Unit test the function.
实现一个函数 add_row_vectors: int list -> int list -> int list 以将两个行向量按元素相加。例如, [1; 1; 1][9; 8; 7] 相加为 [10; 9; 8] 。如果两个向量没有相同数量的条目,则函数的行为是未指定的,也就是说,它可以执行您喜欢的任何操作。提示:有一个使用 List.map2 的优雅的单行解决方案。对功能进行单元测试。


Exercise: matrix add [★★★]
练习:矩阵加法[★★★]

Implement a function add_matrices: int list list -> int list list -> int list list for matrix addition. If the two input matrices are not the same size, the behavior is unspecified. Hint: there is an elegant one-line solution using List.map2 and add_row_vectors. Unit test the function.
实现矩阵加法的函数 add_matrices: int list list -> int list list -> int list list 。如果两个输入矩阵的大小不同,则行为未指定。提示:有一个使用 List.map2add_row_vectors 的优雅的单行解决方案。对功能进行单元测试。


Exercise: matrix multiply [★★★★]
练习:矩阵乘法[★★★★]

Implement a function multiply_matrices: int list list -> int list list -> int list list for matrix multiplication. If the two input matrices are not of sizes that can be multiplied together, the behavior is unspecified. Unit test the function. Hint: define functions for matrix transposition and row vector dot product.
实现矩阵乘法的函数 multiply_matrices: int list list -> int list list -> int list list 。如果两个输入矩阵的大小不能相乘,则行为未指定。对功能进行单元测试。提示:定义矩阵转置和行向量点积的函数。

5. Modular Programming 5. 模块化编程 ¶

When a program is small enough, we can keep all of the details of the program in our heads at once. But real-world applications can be many order of magnitude larger than those we write in college classes. They are simply too large and complex to hold all their details in our heads. They are also written by many programmers. To build large software systems requires techniques we haven’t talked about so far.
当程序足够小时,我们可以立即将程序的所有细节保留在我们的脑海中。但现实世界的应用程序可能比我们在大学课堂上编写的应用程序大很多数量级。它们太大太复杂了,我们无法在头脑中记住它们的所有细节。它们也是由许多程序员编写的。构建大型软件系统需要我们到目前为止还没有讨论过的技术。

One key solution to managing complexity of large software is modular programming: the code is composed of many different code modules that are developed separately. This allows different developers to take on discrete pieces of the system and design and implement them without having to understand all the rest. But to build large programs out of modules effectively, we need to be able to write modules that we can convince ourselves are correct in isolation from the rest of the program. Rather than have to think about every other part of the program when developing a code module, we need to be able to use local reasoning: that is, reasoning about just the module and the contract it needs to satisfy with respect to the rest of the program. If everyone has done their job, separately developed code modules can be plugged together to form a working program without every developer needing to understand everything done by every other developer in the team. This is the key idea of modular programming.
管理大型软件复杂性的一个关键解决方案是模块化编程:代码由许多单独开发的不同代码模块组成。这使得不同的开发人员能够承担系统的离散部分并设计和实现它们,而无需了解其余所有部分。但是,为了有效地用模块构建大型程序,我们需要能够编写能够说服自己在与程序的其余部分隔离的情况下是正确的模块。在开发代码模块时,我们不需要考虑程序的所有其他部分,而是需要能够使用本地推理:也就是说,仅推理模块及其需要满足的其余部分的契约程序。如果每个人都完成了自己的工作,则可以将单独开发的代码模块插入在一起以形成一个工作程序,而无需每个开发人员都需要了解团队中其他开发人员所做的一切。这就是模块化编程的关键思想。

Therefore, to build large programs that work, we must use abstraction to make it manageable to think about the program. Abstraction is simply the removal of detail. A well-written program has the property that we can think about its components (such as functions) abstractly, without concerning ourselves with all the details of how those components are implemented.
因此,为了构建有效的大型程序,我们必须使用抽象来使程序的思考变得易于管理。抽象只是去除细节。一个编写良好的程序具有这样的特性:我们可以抽象地思考它的组件(例如函数),而无需关心这些组件如何实现的所有细节。

Modules are abstracted by giving specifications of what they are supposed to do. A good module specification is clear, understandable, and gives just enough information about what the module does for clients to successfully use it. This abstraction makes the programmer’s job much easier; it is helpful even when there is only one programmer working on a moderately large program, and it is crucial when there is more than one programmer.
模块是通过给出它们应该做什么的规范来抽象的。一个好的模块规范是清晰的、易于理解的,并且提供了足够的关于模块功能的信息,以便客户成功使用它。这种抽象使程序员的工作变得更加容易;即使只有一名程序员在处理一个中等规模的程序时,它也是有帮助的,而当有多于一名程序员时,它是至关重要的。

Industrial-strength languages contain mechanisms that support modular programming. In general (i.e. across programming languages), a module specification is known as an interface, which provides information to clients about the module’s functionality while hiding the implementation. Object-oriented languages support modular programming with classes. The Java interface construct is one example of a mechanism for specifying the interface to a class. A Java interface informs clients of the available functionality in any class that implements it without revealing the details of the implementation. But even just the public methods of a class constitute an interface in the more general sense—an abstract description of what the module can do.
工业强度语言包含支持模块化编程的机制。一般来说(即跨编程语言),模块规范被称为接口,它向客户端提供有关模块功能的信息,同时隐藏实现。面向对象语言支持使用类进行模块化编程。 Java interface 构造是指定类接口的机制的一个示例。 Java interface 通知客户端实现它的任何类中的可用功能,而不透露实现的细节。但即使只是类的公共方法也构成了更一般意义上的接口——模块功能的抽象描述。

Developers working with a module take on distinct roles. Most developers are usually clients of the module who understand the interface but do not need to understand the implementation of the module. A developer who works on the module implementation is naturally called an implementer. The module interface is a contract between the client and the implementer, defining the responsibilities of both. Contracts are very important because they help us to isolate the source of the problem when something goes wrong—and to know who to blame!
使用模块的开发人员扮演着不同的角色。大多数开发人员通常是模块的客户,他们了解接口,但不需要了解模块的实现。负责模块实现的开发人员自然被称为实现者。模块接口是客户端和实现者之间的契约,定义了双方的职责。合同非常重要,因为它们可以帮助我们在出现问题时隔离问题根源,并知道应该责怪谁!

It is good practice to involve both clients and implementers in the design of a module’s interface. Interfaces designed solely by one or the other can be seriously deficient. Each side will have its own view of what the final product should look like, and these may not align! So mutual agreement on the contract is essential. It is also important to think hard about global module structure and interfaces early, because changing an interface becomes more and more difficult as the development proceeds and more of the code comes to depend on it.
让客户和实现者都参与模块接口的设计是一种很好的做法。单独由其中一方设计的界面可能存在严重缺陷。每一方对最终产品的外观都有自己的看法,但这些看法可能不一致!因此,双方就合同达成一致至关重要。尽早认真考虑全局模块结构和接口也很重要,因为随着开发的进行,更改接口变得越来越困难,并且越来越多的代码依赖于它。

Modules should be used only through their declared interfaces, which the language should help to enforce. This is true even when the client and the implementer are the same person. Modules decouple the system design and implementation problem into separate tasks that can be carried out largely independently. When a module is used only through its interface, the implementer has the flexibility to change the module as long as the module still satisfies its interface.
模块只能通过其声明的接口来使用,语言应该帮助强制执行。即使客户和实施者是同一个人也是如此。模块将系统设计和实现问题解耦为可以在很大程度上独立执行的单独任务。当模块仅通过其接口使用时,只要模块仍然满足其接口,实现者就可以灵活地更改模块。

5.1. Module Systems 5.1. 模块系统 ¶

A programming language’s module system is the set of features it provides in support of modular programming. Below are some common concerns of module systems. We focus on Java and OCaml in this discussion, mentioning some of the most related features in the two languages.
编程语言的模块系统是它提供的支持模块化编程的一组功能。以下是模块系统的一些常见问题。在本次讨论中,我们重点关注 Java 和 OCaml,并提到这两种语言中一些最相关的功能。

Namespaces. A namespace provides a set of names that are grouped together, are usually logically related, and are distinct from other namespaces. That enables a name foo in one namespace to have a distinct meaning from foo in another namespace. A namespace is thus a scoping mechanism. Namespaces are essential for modularity. Without them, the names that one programmer chooses could collide with the names another programmer chooses. In Java, classes (and packages) group names. In OCaml, structures (which we will soon study) are similar to classes in that they group names — but without any of the added complexity of object-oriented programming that usually accompanies classes (constructors, static vs. instance members, inheritance, overriding, this, etc.) Structures are the core of the OCaml module system; in fact, we’ve been using them all along without thinking too much about them.
命名空间。命名空间提供一组组合在一起的名称,这些名称通常在逻辑上相关,并且与其他命名空间不同。这使得一个命名空间中的名称 foo 与另一个命名空间中的 foo 具有不同的含义。因此,命名空间是一种范围机制。命名空间对于模块化至关重要。如果没有它们,一个程序员选择的名称可能会与另一位程序员选择的名称发生冲突。在 Java 中,类(和包)组名称。在 OCaml 中,结构(我们将很快研究)与类相似,因为它们对名称进行分组 - 但没有任何通常伴随类的面向对象编程的复杂性(构造函数、静态成员与实例成员、继承、重写、 this 等)结构体是OCaml模块系统的核心;事实上,我们一直在使用它们,但没有过多考虑它们。

Abstraction. An abstraction hides some information while revealing other information. Abstraction thus enables encapsulation, aka information hiding. Usually, abstraction mechanisms for modules allow revealing some names that exist inside the module, but hiding some others. Abstractions therefore describe relationships among modules: there might be many modules that could considered to satisfy a given abstraction. Abstraction is essential for modularity, because it enables implementers of a module to hide the details of the implementation from clients, thus preventing the clients from abusing those details. In a large team, the modules one programmer designs are thereby protected from abuse by another programmer. It also enables clients to be blissfully unaware of those details. So, in a large team, no programmer has to be aware of all the details of all the modules. In Java, interfaces and abstract classes provide abstraction. In OCaml, signatures are used to abstract structures by hiding some of the structure’s names and definitions. Signatures are essentially the types of structures.
抽象。抽象隐藏了一些信息,同时揭示了其他信息。因此,抽象可以实现封装,也称为信息隐藏。通常,模块的抽象机制允许显示模块内部存在的一些名称,但隐藏其他一些名称。因此,抽象描述了模块之间的关系:可能有许多模块可以被认为满足给定的抽象。抽象对于模块化至关重要,因为它使模块的实现者能够向客户端隐藏实现的细节,从而防止客户端滥用这些细节。在一个大型团队中,一个程序员设计的模块可以避免被另一个程序员滥用。它还使客户能够幸福地不知道这些细节。因此,在一个大型团队中,任何程序员都不必了解所有模块的所有细节。在Java中,接口和抽象类提供了抽象。在 OCaml 中,签名用于通过隐藏一些结构的名称和定义来抽象结构。签名本质上是结构的类型。

Code reuse. A module system enables code reuse by providing features that enable code from one module to be used as part of another module without having to copy that code. Code reuse thereby enables programmers to build on the work of others in a way that is maintainable: when the implementer of one module makes an improvement in that module, all the programmers who are reusing that code automatically get the benefit of that improvement. Code reuse is essential for modularity, because it enables “building blocks” that can be assembled and reassembled to form complex pieces of software. In Java, subtyping and inheritance provide code reuse. In OCaml, functors and includes enable code reuse. Functors are like functions, in that they produce new modules out of old modules. Includes are like an intelligent form of copy-paste: they include code from one part of a program in another.
代码重用。模块系统通过提供使一个模块中的代码能够用作另一模块的一部分而无需复制该代码的功能来实现代码重用。因此,代码重用使程序员能够以可维护的方式构建其他人的工作:当一个模块的实现者对该模块进行改进时,所有重用该代码的程序员都会自动获得该改进的好处。代码重用对于模块化至关重要,因为它使“构建块”能够被组装和重新组装以形成复杂的软件片段。在 Java 中,子类型和继承提供了代码重用。在 OCaml 中,函子和包含可实现代码重用。函子与函数类似,它们从旧模块中生成新模块。包含就像一种智能形式的复制粘贴:它们将程序一个部分的代码包含到另一个部分中。

Warning 警告

These analogies between Java and OCaml are necessarily imperfect. You might naturally come away from the above discussion thinking either of the following:
Java 和 OCaml 之间的这些类比必然是不完美的。结束上述讨论后,您可能会自然而然地想到以下任一内容:

  • “Structures are like Java classes, and signatures are like interfaces.”
    “结构就像 Java 类,签名就像接口。”

  • “Structures are like Java objects, and signatures are like classes.”
    “结构就像 Java 对象,签名就像类。”

Both are helpful to a degree, yet both are ultimately wrong. So it might be best to let go of object-oriented programming at this point and come to terms with the OCaml module system in and of itself. Compared to Java, it’s just built different.
两者在一定程度上都有帮助,但最终都是错误的。因此,此时最好放弃面向对象编程并接受 OCaml 模块系统本身。与 Java 相比,它只是构建方式不同。

5.2. Modules
5.2. 模块 ¶

We begin with a couple of examples of the OCaml module system before diving into the details.
在深入讨论细节之前,我们首先从 OCaml 模块系统的几个示例开始。

A structure is simply a collection of definitions, such as:
结构只是定义的集合,例如:

struct
  let inc x = x + 1
  type primary_color = Red | Green | Blue
  exception Oops
end

In a way, the structure is like a record: the structure has some distinct components with names. But unlike a record, it can define new types, exceptions, and so forth.
在某种程度上,该结构就像一个记录:该结构具有一些带有名称的不同组件。但与记录不同的是,它可以定义新类型、异常等。

By itself the code above won’t compile, because structures do not have the same first-class status as values like integers or functions. You can’t just enter that code in utop, or pass that structure to a function, etc. What you can do is bind the structure to a name:
上面的代码本身无法编译,因为结构不具有与整数或函数等值相同的第一级地位。您不能只在 utop 中输入该代码,或将该结构传递给函数等。您可以做的是将结构绑定到名称:

module MyModule = struct
  let inc x = x + 1
  type primary_color = Red | Green | Blue
  exception Oops
end
module MyModule :
  sig
    val inc : int -> int
    type primary_color = Red | Green | Blue
    exception Oops
  end

The output from OCaml has the form:
OCaml 的输出具有以下形式:

module MyModule : sig ... end

This indicates that MyModule has been defined, and that it has been inferred to have the module type that appears to the right of the colon. That module type is written as signature:
这表明 MyModule 已被定义,并且已推断出具有出现在冒号右侧的模块类型。该模块类型被写为签名:

sig
  val inc : int -> int
  type primary_color = Red | Green | Blue
  exception Oops
end

The signature itself is a collection of specifications. The specifications for variant types and exceptions are simply their original definitions, so primary_color and Oops are no different than they were in the original structure. The specification for inc though is written with the val keyword, exactly as the toplevel would respond if we defined inc in it.
签名本身是规范的集合。变体类型和异常的规范只是它们的原始定义,因此 primary_colorOops 与原始结构中的没有什么不同。 inc 的规范是用 val 关键字编写的,与我们在其中定义 inc 时顶层的响应完全相同。

Note 笔记

This use of the word “specification” is perhaps confusing, since many programmers would use that word to mean “the comments specifying the behavior of a function.” But if we broaden our sight a little, we could allow that the type of a function is part of its specification. So it’s at least a related sense of the word.
“规范”一词的使用可能会令人困惑,因为许多程序员会使用该词来表示“指定函数行为的注释”。但如果我们稍微拓宽一下视野,我们就可以允许函数的类型是其规范的一部分。所以它至少是这个词的相关含义。

The definitions in a module are usually more closely related than those in MyModule. Often a module will implement some data structure. For example, here is a module for stacks implemented as linked lists:
模块中的定义通常比 MyModule 中的定义更密切相关。通常一个模块会实现一些数据结构。例如,这是一个以链表形式实现的堆栈模块:

module ListStack = struct
  (** [empty] is the empty stack. *)
  let empty = []

  (** [is_empty s] is whether [s] is empty. *)
  let is_empty = function [] -> true | _ -> false

  (** [push x s] pushes [x] onto the top of [s]. *)
  let push x s = x :: s

  (** [Empty] is raised when an operation cannot be applied
      to an empty stack. *)
  exception Empty

  (** [peek s] is the top element of [s].
      Raises [Empty] if [s] is empty. *)
  let peek = function
    | [] -> raise Empty
    | x :: _ -> x

  (** [pop s] is all but the top element of [s].
      Raises [Empty] if [s] is empty. *)
  let pop = function
    | [] -> raise Empty
    | _ :: s -> s
end
module ListStack :
  sig
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    exception Empty
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end

Important 重要的

The specification of pop might surprise you. Note that it does not return the top element. That’s the job of peek. Instead, pop returns all but the top element.
pop 的规范可能会让您感到惊讶。请注意,它不返回顶部元素。这就是 peek 的工作。相反, pop 返回除顶部元素之外的所有元素。

We can then use that module to manipulate a stack:
然后我们可以使用该模块来操作堆栈:

ListStack.push 2 (ListStack.push 1 ListStack.empty)
- : int list = [2; 1]

Warning 警告

There’s a common confusion lurking here for those programmers coming from object-oriented languages. It’s tempting to think of ListStack as being an object on which you invoke methods. Indeed ListStack.push vaguely looks like we’re invoking a push method on a ListStack object. But that’s not what is happening. In an OO language you could instantiate many stack objects. But here, there is only one ListStack. Moreover it is not an object, in large part because it has no notion of a this or self keyword to denote the receiving object of the method call.
对于那些来自面向对象语言的程序员来说,这里潜伏着一个常见的困惑。人们很容易将 ListStack 视为调用其方法的对象。事实上, ListStack.push 隐约看起来像是我们在 ListStack 对象上调用 push 方法。但事实并非如此。在面向对象语言中,您可以实例化许多堆栈对象。但这里只有一个 ListStack 。此外,它不是一个对象,很大程度上是因为它没有 thisself 关键字的概念来表示方法调用的接收对象。

That’s admittedly rather verbose code. Soon we’ll see several solutions to that problem, but for now here’s one:
无可否认,这是相当冗长的代码。很快我们就会看到该问题的多种解决方案,但目前只有一个:

ListStack.(push 2 (push 1 empty))
- : int list = [2; 1]

By writing ListStack.(e), all the names from ListStack become usable in e without needing to write the prefix ListStack. each time. Another improvement could be using the pipeline operator:
通过编写 ListStack.(e)ListStack 中的所有名称都可以在 e 中使用,而无需每次都编写前缀 ListStack. 。另一个改进可能是使用管道运算符:

ListStack.(empty |> push 1 |> push 2)
- : int list = [2; 1]

Now we can read the code left-to-right without having to parse parentheses. Nice.
现在我们可以从左到右阅读代码,而不必解析括号。好的。

Warning 警告

There’s another common OO confusion lurking here. It’s tempting to think of ListStack as being a class from which objects are instantiated. That’s not the case though. Notice how there is no new operator used to create a stack above, nor any constructors (in the OO sense of that word).
这里潜伏着另一个常见的面向对象混淆。人们很容易将 ListStack 视为实例化对象的类。但事实并非如此。请注意上面没有用于创建堆栈的 new 运算符,也没有任何构造函数(在该词的 OO 意义上)。

Modules are considerably more basic than classes. A module is just a collection of definitions in its own namespace. In ListStack, we have some definitions of functions—push, pop, etc.—and one value, empty.
模块比类更加基础。模块只是其自己的命名空间中的定义的集合。在 ListStack 中,我们有一些函数定义 - pushpop 等 - 以及一个值 empty

So whereas in Java we might create a couple of stacks using code like this:
因此,在 Java 中,我们可以使用如下代码创建几个堆栈:

Stack s1 = new Stack();
s1.push(1);
s1.push(2);
Stack s2 = new Stack();
s2.push(3);

In OCaml the same stacks could be created as follows:
在 OCaml 中,可以按如下方式创建相同的堆栈:

let s1 = ListStack.(empty |> push 1 |> push 2)
let s2 = ListStack.(empty |> push 3)
val s1 : int list = [2; 1]
val s2 : int list = [3]

5.2.1. Module Definitions
5.2.1. 模块定义 ¶

The module definition keyword is much like the let definition keyword that we learned before. (The OCaml designers hypothetically could have chosen to use let_module instead of module to emphasize the similarity.) The difference is just that:
module 定义关键字与我们之前学过的 let 定义关键字非常相似。 (假设 OCaml 设计者可以选择使用 let_module 而不是 module 来强调相似性。)区别在于:

  • let binds a value to a name, whereas
    let 将值绑定到名称,而

  • module binds a module value to a name.
    module 将模块值绑定到名称。

Syntax.

The most common syntax for a module definition is simply:
模块定义最常见的语法很简单:

module ModuleName = struct
  module_items
end

where module_items inside a structure can include let definitions, type definitions, and exception definitions, as well as nested module definitions. Module names must begin with an uppercase letter, and idiomatically they use CamelCase rather than Snake_case.
其中结构内的 module_items 可以包含 let 定义、 type 定义和 exception 定义以及嵌套的 module 而不是 Snake_case

But a more accurate version of the syntax would be:
但更准确的语法版本是:

module ModuleName = module_expression

where a struct is just one sort of module_expression. Here’s another: the name of an already defined module. For example, you can write module L = List if you’d like a short alias for the List module. We’ll see other sorts of module expressions later in this section and chapter.
其中 struct 只是 module_expression 的一种。这是另一个:已定义模块的名称。例如,如果您想要 List 模块的简短别名,则可以编写 module L = List 。我们将在本节和本章的后面看到其他类型的模块表达式。

The definitions inside a structure can optionally be terminated by ;; as in the toplevel:
结构内的定义可以选择以 ;; 终止,如顶层所示:

module M = struct
  let x = 0;;
  type t = int;;
end
module M : sig val x : int type t = int end

Sometimes that can be useful to add temporarily if you are trying to diagnose a syntax error. It will help OCaml understand that you want two definitions to be syntactically separate. After fixing whatever the underlying error is, though, you can remove the ;;.
有时,如果您尝试诊断语法错误,临时添加可能会很有用。它将帮助 OCaml 理解您希望两个定义在语法上是独立的。不过,修复潜在错误后,您可以删除 ;;

One use case for ;; is if you want to evaluate an expression as part of a module:
;; 的一个用例是如果您想要将表达式作为模块的一部分进行计算:

module M = struct
  let x = 0;;
  assert (x = 0);;
end
module M : sig val x : int end

But that can be rewritten without ;; as:
但可以在没有 ;; 的情况下将其重写为:

module M = struct
  let x = 0
  let _ = assert (x = 0)
end
module M : sig val x : int end

Structures can also be written on a single line, with optional ;; between items for readability:
结构也可以写在一行上,在项目之间使用可选的 ;; 以提高可读性:

module N = struct let x = 0 let y = 1 end
module O = struct let x = 0;; let y = 1 end
module N : sig val x : int val y : int end
module O : sig val x : int val y : int end

An empty structure is permitted:
允许使用空结构:

module E = struct end
module E : sig end

Dynamic semantics. 动态语义。

We already know that expressions are evaluated to values. Similarly, a module expression is evaluated to a module value or just “module” for short. The only interesting kind of module expression we have so far, from the perspective of evaluation anyway, is the structure. Evaluation of structures is easy: just evaluate each definition in it, in the order they occur. Because of that, earlier definitions are therefore in scope in later definitions, but not vice versa. So this module is fine:
我们已经知道表达式被计算为值。类似地,模块表达式被计算为模块值或简称为“模块”。无论如何,从评估的角度来看,迄今为止我们拥有的唯一有趣的模块表达式是结构。结构的评估很容易:只需按照它们出现的顺序评估其中的每个定义即可。因此,较早的定义在较晚的定义范围内,但反之则不然。所以这个模块很好:

module M = struct
  let x = 0
  let y = x
end
module M : sig val x : int val y : int end

But this module is not, because at the time the let definition of x is being evaluated, y has not yet been bound:
但这个模块不是,因为在评估 xlet 定义时, y 尚未绑定:

module M = struct
  let x = y
  let y = 0
end
File "[13]", line 2, characters 10-11:
2 |   let x = y
              ^
Error: Unbound value y

Of course, mutual recursion can be used if desired:
当然,如果需要的话可以使用相互递归:

module M = struct
  (* Requires: input is non-negative. *)
  let rec even = function 
    | 0 -> true 
    | n -> odd (n - 1)
  and odd = function 
    | 0 -> false 
    | n -> even (n - 1)
end
module M : sig val even : int -> bool val odd : int -> bool end

Static semantics. 静态语义。

A structure is well typed if all the definitions in it are themselves well-typed, according to all the typing rules we have already learned.
根据我们已经学过的所有类型规则,如果结构中的所有定义本身都是类型良好的,那么该结构就是类型良好的。

As we’ve seen in toplevel output, the module type of a structure is a signature. There’s more to module types than that, though. Let’s put that off for a moment to first talk about scope.
正如我们在顶级输出中看到的,结构的模块类型是签名。不过,模块类型还有更多内容。让我们先讨论一下范围。

5.2.2. Scope and Open
5.2.2. 范围和开放 ¶

After a module M has been defined, you can access the names within it using the dot operator. For example:
定义模块 M 后,您可以使用点运算符访问其中的名称。例如:

module M = struct let x = 42 end
module M : sig val x : int end
M.x
- : int = 42

Of course from outside the module the name x by itself is not meaningful:
当然,从模块外部来看,名称 x 本身是没有意义的:

x
File "[17]", line 1, characters 0-1:
1 | x
    ^
Error: Unbound value x

But you can bring all of the definitions of a module into the current scope using open:
但是您可以使用 open 将模块的所有定义带入当前范围:

open M
x
- : int = 42

Opening a module is like writing a local definition for each name defined in the module. For example, open String brings all the definitions from the String module into scope, and has an effect similar to the following on the local namespace:
打开模块就像为模块中定义的每个名称编写本地定义一样。例如, open String 将 String 模块中的所有定义带入作用域,并对本地命名空间产生类似于以下内容的效果:

let length = String.length
let get = String.get
let lowercase_ascii = String.lowercase_ascii
...

If there are types, exceptions, or modules defined in a module, those also are brought into scope with open.
如果模块中定义了类型、异常或模块,它们也会被纳入 open 的范围内。

The Always-Open Module. There is a special module called Stdlib that is automatically opened in every OCaml program. It contains the “built-in” functions and operators. You therefore never need to prefix any of the names it defines with Stdlib., though you could do so if you ever needed to unambiguously identify a name from it. In earlier days, this module was named Pervasives, and you might still see that name in some code bases.
始终开放的模块。有一个名为 Stdlib 的特殊模块,在每个 OCaml 程序中都会自动打开。它包含“内置”函数和运算符。因此,您永远不需要在它定义的任何名称前添加 Stdlib. 前缀,但如果您需要从中明确标识名称,则可以这样做。在早期,该模块被命名为 Pervasives ,您可能仍然在某些代码库中看到该名称。

Open as a Module Item. An open is another sort of module_item. So we can open one module inside another:
作为模块项打开。 openmodule_item 的另一种形式。因此我们可以在另一个模块中打开一个模块:

module M = struct
  open List

  (** [uppercase_all lst] upper-cases all the elements of [lst]. *)
  let uppercase_all = map String.uppercase_ascii
end
module M : sig val uppercase_all : string list -> string list end

Since List is open, the name map from it is in scope. But what if we wanted to get rid of the String. as well?
由于 List 已打开,因此它的名称 map 在范围内。但是如果我们也想去掉 String. 该怎么办?

module M = struct
  open List
  open String

  (** [uppercase_all lst] upper-cases all the elements of [lst]. *)
  let uppercase_all = map uppercase_ascii
end
File "[21]", line 6, characters 26-41:
6 |   let uppercase_all = map uppercase_ascii
                              ^^^^^^^^^^^^^^^
Error: This expression has type string -> string
       but an expression was expected of type char -> char
       Type string is not compatible with type char 

Now we have a problem, because String also defines the name map, but with a different type than List. As usual a later definition shadows an earlier one, so it’s String.map that gets chosen instead of List.map as we intended.
现在我们有一个问题,因为 String 也定义了名称 map ,但类型与 List 不同。像往常一样,后面的定义会掩盖前面的定义,因此选择的是 String.map 而不是我们想要的 List.map

If you’re using many modules inside your code, chances are you’ll have at least one collision like this. Often it will be with a standard higher-order function like map that is defined in many library modules.
如果您在代码中使用许多模块,则很可能至少会发生一次这样的冲突。通常它会使用标准的高阶函数,例如在许多库模块中定义的 map

Tip

It is therefore generally good practice not to open all the modules you’re going to use at the top of a .ml file or structure. This is perhaps different than how you’re used to working with languages like Java, where you might import many packages with *. Instead, it’s good to restrict the scope in which you open modules.
因此,通常最好不要将要使用的所有模块 open 放在 .ml 文件或结构的顶部。这可能与您习惯使用 Java 等语言的方式不同,在 Java 中您可能会 import 许多带有 * 的包。相反,最好限制打开模块的范围。

Limiting the Scope of Open. We’ve already seen one way of limiting the scope of an open: M.(e). Inside e all the names from module M are in scope. This is useful for briefly using M in a short expression:
限制开放范围。我们已经看到了一种限制打开范围的方法: M.(e) 。在 e 内,模块 M 中的所有名称都在范围内。这对于在简短表达式中短暂使用 M 非常有用:

(* remove surrounding whitespace from [s] and convert it to lower case *)
let s = "BigRed "
let s' = s |> String.trim |> String.lowercase_ascii (* long way *)
let s'' = String.(s |> trim |> lowercase_ascii) (* short way *)
val s : string = "BigRed "
val s' : string = "bigred"
val s'' : string = "bigred"

But what if you want to bring a module into scope for an entire function, or some other large block of code? The (admittedly strange) syntax for that is let open M in e. It makes all the names from M be in scope in e. For example:
但是,如果您想将模块纳入整个函数或其他大型代码块的范围,该怎么办? (诚​​然很奇怪)的语法是 let open M in e 。它使 M 中的所有名称都在 e 的范围内。例如:

(** [lower_trim s] is [s] in lower case with whitespace removed. *)
let lower_trim s =
  let open String in
  s |> trim |> lowercase_ascii
val lower_trim : string -> string = <fun>

Going back to our uppercase_all example, it might be best to eschew any kind of opening and simply to be explicit about which module we are using where:
回到我们的 uppercase_all 示例,最好避免任何类型的打开,而只是明确我们在何处使用哪个模块:

module M = struct
  (** [uppercase_all lst] upper-cases all the elements of [lst]. *)
  let uppercase_all = List.map String.uppercase_ascii
end
module M : sig val uppercase_all : string list -> string list end

5.2.3. Module Type Definitions
5.2.3. 模块类型定义 ¶

We’ve already seen that OCaml will infer a signature as the type of a module. Let’s now see how to write those modules types ourselves. As an example, here is a module type for our list-based stacks:
我们已经看到 OCaml 会将签名推断为模块的类型。现在让我们看看如何自己编写这些模块类型。作为示例,以下是基于列表的堆栈的模块类型:

module type LIST_STACK = sig
  exception Empty
  val empty : 'a list
  val is_empty : 'a list -> bool
  val push : 'a -> 'a list -> 'a list
  val peek : 'a list -> 'a
  val pop : 'a list -> 'a list
end
module type LIST_STACK =
  sig
    exception Empty
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end

Now that we have both a module and a module type for list-based stacks, we should move the specification comments from the structure into the signature. Those comments are properly part of the specification of the names in the signature. They specify behavior, thus augmenting the specification of types provided by the val declarations.
现在我们已经有了基于列表的堆栈的模块和模块类型,我们应该将规范注释从结构移到签名中。这些注释是签名中名称规范的正确组成部分。它们指定行为,从而增强了 val 声明提供的类型规范。

module type LIST_STACK = sig
  (** [Empty] is raised when an operation cannot be applied
      to an empty stack. *)
  exception Empty

  (** [empty] is the empty stack. *)
  val empty : 'a list

  (** [is_empty s] is whether [s] is empty. *)
  val is_empty : 'a list -> bool

  (** [push x s] pushes [x] onto the top of [s]. *)
  val push : 'a -> 'a list -> 'a list

  (** [peek s] is the top element of [s].
      Raises [Empty] if [s] is empty. *)
  val peek : 'a list -> 'a

  (** [pop s] is all but the top element of [s].
      Raises [Empty] if [s] is empty. *)
  val pop : 'a list -> 'a list
end

module ListStack = struct
  let empty = []

  let is_empty = function [] -> true | _ -> false

  let push x s = x :: s

  exception Empty

  let peek = function
    | [] -> raise Empty
    | x :: _ -> x

  let pop = function
    | [] -> raise Empty
    | _ :: s -> s
end
module type LIST_STACK =
  sig
    exception Empty
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end
module ListStack :
  sig
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    exception Empty
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end

Nothing so far, however, tells OCaml that there is a relationship between LIST_STACK and ListStack. If we want OCaml to ensure that ListStack really does have the module type specified by LIST_STACK, we can add a type annotation in the first line of the module definition:
然而,到目前为止,没有任何信息告诉 OCaml LIST_STACKListStack 之间存在关系。如果我们希望 OCaml 确保 ListStack 确实具有 LIST_STACK 指定的模块类型,我们可以在 module 定义的第一行添加类型注释:

module ListStack : LIST_STACK = struct
  let empty = []

  let is_empty = function [] -> true | _ -> false

  let push x s = x :: s

  exception Empty

  let peek = function
    | [] -> raise Empty
    | x :: _ -> x

  let pop = function
    | [] -> raise Empty
    | _ :: s -> s
end
module ListStack : LIST_STACK

The compiler agrees that the module ListStack does define all the items specified by LIST_STACK with appropriate types. If we had accidentally omitted some item, the type annotation would have been rejected:
编译器同意模块 ListStack 确实使用适当的类型定义了 LIST_STACK 指定的所有项。如果我们不小心遗漏了某些项目,类型注释将被拒绝:

module ListStack : LIST_STACK = struct
  let empty = []

  let is_empty = function [] -> true | _ -> false

  let push x s = x :: s

  exception Empty

  let peek = function
    | [] -> raise Empty
    | x :: _ -> x

  (* [pop] is missing *)
end
File "[28]", lines 1-15, characters 32-3:
 1 | ................................struct
 2 |   let empty = []
 3 | 
 4 |   let is_empty = function [] -> true | _ -> false
 5 | 
...
12 |     | x :: _ -> x
13 | 
14 |   (* [pop] is missing *)
15 | end
Error: Signature mismatch:
       ...
       The value `pop' is required but not provided
       File "[26]", line 21, characters 2-30: Expected declaration

Syntax.

The most common syntax for a module type is simply:
模块类型最常见的语法很简单:

module type ModuleTypeName = sig
  specifications
end

where specifications inside a signature can include val declarations, type definitions, exception definitions, and nested module type definitions. Like structures, a signature can be written on many lines or just one line, and the empty signature sig end is allowed.
其中签名内的 specifications 可以包含 val 声明、类型定义、异常定义和嵌套 module type 定义。与结构体一样,签名可以写在多行或仅一行上,并且允许使用空签名 sig end

But, as we saw with module definitions, a more accurate version of the syntax would be:
但是,正如我们在模块定义中看到的,更准确的语法版本是:

module type ModuleTypeName = module_type

where a signature is just one sort of module_type. Another would be the name of an already defined module type—e.g., module type LS = LIST_STACK. We’ll see other module types later in this section and chapter.
其中签名只是 module_type 的一种。另一个是已定义的模块类型的名称,例如 module type LS = LIST_STACK 。我们将在本节和本章的后面看到其他模块类型。

By convention, module type names are usually CamelCase, like module names. So why did we use ALL_CAPS above for LIST_STACK? It was to avoid a possible point of confusion in that example, which we now illustrate. We could instead have used ListStack as the name of both the module and the module type:
按照惯例,模块类型名称通常为 CamelCase ,就像模块名称一样。那么为什么我们使用上面的 ALL_CAPS 作为 LIST_STACK 呢?这是为了避免该示例中可能出现的混淆点,我们现在对此进行说明。我们可以使用 ListStack 作为模块和模块类型的名称:

module type ListStack = sig ... end
module ListStack : ListStack = struct ... end

In OCaml the namespaces for modules and module types are distinct, so it’s perfectly valid to have a module named ListStack and a module type named ListStack. The compiler will not get confused about which you mean, because they occur in distinct syntactic contexts. But as a human you might well get confused by those seemingly overloaded names.
在 OCaml 中,模块和模块类型的命名空间是不同的,因此拥有名为 ListStack 的模块和名为 ListStack 的模块类型是完全有效的。编译器不会对您的意思感到困惑,因为它们出现在不同的语法上下文中。但作为一个人,你很可能会对那些看似超载的名称感到困惑。

Note 笔记

The use of ALL_CAPS for module types was at one point common, and you might see it still. It’s an older convention from Standard ML. But the social conventions of all caps have changed since those days. To modern readers, a name like LIST_STACK might feel like your code is impolitely shouting at you. That is a connotation that evolved in the 1980s. Older programming languages (e.g., Pascal, COBOL, FORTRAN) commonly used all caps for keywords and even their own names. Modern languages still idiomatically use all caps for constants—see, for example, Java’s Math.PI or Python’s style guide.
在模块类型中使用 ALL_CAPS 一度很常见,而且您可能仍然会看到它。这是标准机器学习的一个较旧的约定。但从那时起,所有大写字母的社会惯例已经发生了变化。对于现代读者来说,像 LIST_STACK 这样的名称可能会让人觉得你的代码在无礼地对你大喊大叫。这是 20 世纪 80 年代演变的内涵。较旧的编程语言(例如 Pascal、COBOL、FORTRAN)通常使用全部大写的关键字,甚至它们自己的名称。现代语言仍然习惯性地使用全部大写的常量,例如,请参阅 Java 的 Math.PI 或 Python 的样式指南。

More Syntax. 更多语法。

We should also add syntax now for module type annotations. Module definitions may include an optional type annotation:
我们现在还应该为模块类型注释添加语法。模块定义可以包括可选的类型注释:

module ModuleName : module_type = module_expression

And module expressions may include manual type annotations:
并且模块表达式可能包含手动类型注释:

(module_expression : module_type)

That syntax is analogous to how we can write (e : t) to manually specify the type t of an expression e.
该语法类似于我们如何编写 (e : t) 来手动指定表达式 e 的类型 t

Here are a few examples to show how that syntax can be used:
以下是一些示例,展示了如何使用该语法:

module ListStackAlias : LIST_STACK = ListStack
(* equivalently *)
module ListStackAlias = (ListStack : LIST_STACK)

module M : sig val x : int end = struct let x = 42 end
(* equivalently *)
module M = (struct let x = 42 end : sig val x : int end)
module ListStackAlias : LIST_STACK
module ListStackAlias : LIST_STACK
module M : sig val x : int end
module M : sig val x : int end

And, module types can include nested module specifications:
并且,模块类型可以包含嵌套模块规范:

module type X = sig
  val x : int
end

module type T = sig
  module Inner : X
end

module M : T = struct
  module Inner : X = struct
    let x = 42
  end
end
module type X = sig val x : int end
module type T = sig module Inner : X end
module M : T

In the example above, T specifies that there must be an inner module named Inner whose module type is X. Here, the type annotation is mandatory, because otherwise nothing would be known about Inner. In implementing T, module M therefore has to provide a module (i) with that name, which also (ii) meets the specifications of module type X.
在上面的示例中, T 指定必须有一个名为 Inner 的内部模块,其模块类型为 X 。在这里,类型注释是强制性的,因为否则将无法了解 Inner 。因此,在实现 T 时,模块 M 必须提供具有该名称的模块 (i),并且 (ii) 满足模块类型 X 的规范。

Dynamic semantics. 动态语义。

Since module types are in fact types, they are not evaluated. They have no dynamic semantics.
由于模块类型实际上是类型,因此不会对它们进行评估。它们没有动态语义。

Static semantics. 静态语义。

Earlier in this section we delayed discussing the static semantics of module expressions. Now that we have learned about module types, we can return to that discussion. We do so, next, in its own section, because the discussion will be lengthy.
在本节前面,我们推迟讨论模块表达式的静态语义。现在我们已经了解了模块类型,我们可以回到那个讨论了。接下来,我们将在其自己的部分中这样做,因为讨论将很冗长。

5.2.4. Module Type Semantics
5.2.4. 模块类型语义 ¶

If M is just a struct block, its module type is whatever signature the compiler infers for it. But that can be changed by module type annotations. The key question we have to answer is: what does a type annotation mean for modules? That is, what does it mean when we write the : T in module M : T = ...?
如果 M 只是一个 struct 块,则其模块类型是编译器为其推断的任何签名。但这可以通过模块类型注释来改变。我们必须回答的关键问题是:类型注释对于模块意味着什么?也就是说,当我们将 : T 写在 module M : T = ... 中时,它意味着什么?

There are two properties the compiler guarantees:
编译器保证两个属性:

  1. Signature matching: every name declared in T is defined in M at the same or a more general type.
    签名匹配: T 中声明的每个名称都在 M 中以相同或更通用的类型定义。

  2. Opacity: any name defined in M that does not appear in T is not visible to code outside of M.
    不透明度: M 中定义的任何未出现在 T 中的名称对于 M 之外的代码不可见。

But a more complete answer turns out to involve subtyping, which is a concept you’ve probably seen before in an object-oriented language. We’re going to take a brief detour into that realm now, then come back to OCaml and modules.
但更完整的答案涉及子类型,这是您以前可能在面向对象语言中见过的概念。我们现在将简短地绕道进入这个领域,然后回到 OCaml 和模块。

In Java, the extends keyword creates subtype relationships between classes:
在 Java 中, extends 关键字创建类之间的子类型关系:

class C { }
class D extends C { }

D d = new D();
C c = d;

Subtyping is what permits the assignment of d to c on the last line of that example. Because D extends C, Java considers D to be a subtype of C, and therefore permits an object instantiated from D to be used any place where an object instantiated from C is expected. It’s up to the programmer of D to ensure that doesn’t lead to any run-time errors, of course. The methods of D have to ensure that class invariants of C hold, for example. So by writing D extends C, the programmer is taking on some responsibility, and in turn gaining some flexibility by being able to write such assignment statements.
子类型允许在该示例的最后一行将 d 分配给 c 。由于 D 扩展了 C ,Java 认为 DC 的子类型,因此允许从 D 实例化对象的地方。当然, D 的程序员需要确保这不会导致任何运行时错误。例如, D 的方法必须确保 C 的类不变量成立。因此,通过编写 D extends C ,程序员承担了一些责任,并通过能够编写此类赋值语句而获得了一些灵活性。

So what is a “subtype”? That notion is in many ways dependent on the language. For a language-independent notion, we turn to Barbara Liskov. She won the Turing Award in 2008 in part for her work on object-oriented language design. Twenty years before that, she invented what is now called the Liskov Substitution Principle to explain subtyping. It says that if S is a subtype of T, then substituting an object of type S for an object of type T should not change any desirable behaviors of a program. You can see that at work in the Java example above, both in terms of what the language allows and what the programmer must guarantee.
那么什么是“亚型”呢?这个概念在很多方面取决于语言。对于独立于语言的概念,我们求助于芭芭拉·利斯科夫(Barbara Liskov)。她于 2008 年获得图灵奖,部分原因是她在面向对象语言设计方面的工作。二十年前,她发明了现在所谓的里氏替换原理来解释子类型。它表示如果 ST 的子类型,则不应将 S 类型的对象替换为 T 类型的对象更改程序的任何所需行为。您可以在上面的 Java 示例中看到这一点,无论是语言允许的内容还是程序员必须保证的内容。

The particular flavor of subtyping in Java is called nominal subtyping, which is to say, it is based on names. In our example, D is a subtype of C just because of the way the names were declared. The programmer decreed that subtype relationship, and the language accepted the decree without question. Indeed the only subtype relationships that exist are those that have been decreed by name through such uses of extends and implements.
Java 中子类型的特殊风格称为名义子类型,也就是说,它基于名称。在我们的示例中, DC 的子类型,只是因为名称的声明方式不同。程序员规定了子类型关系,语言毫无疑问地接受了该规定。事实上,唯一存在的子类型关系是那些通过使用 extendsimplements 来按名称规定的子类型关系。

Now it’s time to return to OCaml. Its module system also uses subtyping, with the same underlying intuition about the Liskov Substitution Principle. But OCaml uses a different flavor called structural subtyping. That is, it is based on the structure of modules rather than their names. “Structure” here simply means the definitions contained in the module. Those definitions are used to determine whether (M : T) is acceptable as a type annotation, where M is a module and T is a module type.
现在是时候回到 OCaml 了。它的模块系统也使用子类型,与里氏替换原理具有相同的基本直觉。但 OCaml 使用了一种不同的风格,称为结构子类型。也就是说,它基于模块的结构而不是它们的名称。这里的“结构”仅指模块中包含的定义。这些定义用于确定 (M : T) 是否可以接受作为类型注释,其中 M 是模块, T 是模块类型。

Let’s play with this idea of structure through several examples, starting with this module:
让我们从这个模块开始,通过几个例子来探讨这种结构的想法:

module M = struct
  let x = 0
  let z = 2
end
module M : sig val x : int val z : int end

Module M contains two definitions. You can see those in the signature for the module that OCaml outputs: it contains x : int and z : int. Because of the former, the module type annotation below is accepted:
模块 M 包含两个定义。您可以在 OCaml 输出的模块的签名中看到这些:它包含 x : intz : int 。由于前者,以下模块类型注释被接受:

module type X = sig
  val x : int
end

module MX = (M : X)
module type X = sig val x : int end
module MX : X

Module type X requires a module item named x with type int. Module M does contain such an item. So (M : X) is valid. The same would work for z:
模块类型 X 需要名为 x 且类型为 int 的模块项。模块 M 确实包含这样的项目。所以 (M : X) 是有效的。对于 z 也同样有效:

module type Z = sig
  val z : int
end

module MZ = (M : Z)
module type Z = sig val z : int end
module MZ : Z

Or for both x and z:
或者对于 xz

module type XZ = sig
  val x : int
  val z : int
end

module MXZ = (M : XZ)
module type XZ = sig val x : int val z : int end
module MXZ : XZ

But not for y, because M contains no such item:
但不适用于 y ,因为 M 不包含此类项目:

module type Y = sig
  val y : int
end

module MY = (M : Y)
module type Y = sig val y : int end
File "[35]", line 5, characters 13-14:
5 | module MY = (M : Y)
                 ^
Error: Signature mismatch:
       Modules do not match:
         sig val x : int val z : int end
       is not included in
         Y
       The value `y' is required but not provided
       File "[35]", line 2, characters 2-13: Expected declaration

Take a close look at that error message. Learning to read such errors on small examples will help you when they appear in large bodies of code. OCaml is comparing two signatures, corresponding to the two expressions on either side of the colon in (M : Y). The line
仔细查看该错误消息。当这些错误出现在大量代码中时,学习在小示例中阅读此类错误将对您有所帮助。 OCaml 正在比较两个签名,对应于 (M : Y) 中冒号两侧的两个表达式。线路

sig val x : int val z : int end

is the signature that OCaml is using for M. Since M is a module, that signature is just the names and types as they were defined in M. OCaml compares that signature to Y, and discovers a mismatch:
是 OCaml 用于 M 的签名。由于 M 是一个模块,因此该签名只是 M 中定义的名称和类型。 OCaml 将该签名与 Y 进行比较,并发现不匹配:

The value `y' is required but not provided

That’s because Y requires y but M provides no such definition.
这是因为 Y 需要 yM 没有提供这样的定义。

Here’s another error message to practice reading:
这是另一个用于练习阅读的错误消息:

module type Xstring = sig
  val x : string
end

module MXstring = (M : Xstring)
module type Xstring = sig val x : string end
File "[36]", line 5, characters 19-20:
5 | module MXstring = (M : Xstring)
                       ^
Error: Signature mismatch:
       Modules do not match:
         sig val x : int val z : int end
       is not included in
         Xstring
       Values do not match: val x : int is not included in val x : string
       The type int is not compatible with the type string
       File "[36]", line 2, characters 2-16: Expected declaration
       File "[31]", line 2, characters 6-7: Actual declaration

This time the error is
这次的错误是

Values do not match: val x : int is not included in val x : string

The error changed, because M does provide a definition of x, but at a different type than Xstring requires. That’s what “is not included in” means here. So why doesn’t OCaml say something a little more straightforward, like “is not the same as”? It’s because the types do not have to be exactly the same. If the provided value’s type is polymorphic, it suffices for the required value’s type to be an instantiation of that polymorphic type.
错误发生了变化,因为 M 确实提供了 x 的定义,但与 Xstring 所需的类型不同。这就是“不包括在内”的意思。那么为什么 OCaml 不说一些更直接的东西,比如“不等于”呢?这是因为类型不必完全相同。如果提供的值的类型是多态的,则所需值的类型只要是该多态类型的实例就足够了。

For example, if a signature requires a type int -> int, it suffices for a structure to provide a value of type 'a -> 'a:
例如,如果签名需要类型 int -> int ,那么结构体提供 'a -> 'a 类型的值就足够了:

module type IntFun = sig
  val f : int -> int
end

module IdFun = struct
  let f x = x
end

module Iid = (IdFun : IntFun)
module type IntFun = sig val f : int -> int end
module IdFun : sig val f : 'a -> 'a end
module Iid : IntFun

So far all these examples were just a matter of comparing the definitions required by a signature to the definitions provided by a structure. But here’s an example that might be surprising:
到目前为止,所有这些示例只是将签名所需的定义与结构提供的定义进行比较。但这里有一个可能令人惊讶的例子:

module MXZ' = ((M : X) : Z)
File "[38]", line 1, characters 15-22:
1 | module MXZ' = ((M : X) : Z)
                   ^^^^^^^
Error: Signature mismatch:
       Modules do not match: X is not included in Z
       The value `z' is required but not provided
       File "[33]", line 2, characters 2-13: Expected declaration

Why does OCaml complain that z is required but not provided? We know from the definition of M that it indeed does have a value z : int. Yet the error message perhaps strangely claims:
为什么 OCaml 抱怨 z 是必需的但未提供?从 M 的定义中我们知道它确实有一个值 z : int 。然而,错误消息可能奇怪地声称:

The value `z' is required but not provided.

The reason for this error is that we’ve already supplied the type annotation X in the module expression (M : X). That causes the module expression to be known only at the module type X. In other words, we’ve forgotten irrevocably about the existence of z after that annotation. All that is known is that the module has items required by X.
出现此错误的原因是我们已经在模块表达式 (M : X) 中提供了类型注释 X 。这导致模块表达式仅在模块类型 X 处已知。换句话说,我们已经不可挽回地忘记了该注释之后 z 的存在。所知道的是该模块具有 X 所需的项目。

After all those examples, here are the static semantics of module type annotations:
在所有这些示例之后,以下是模块类型注释的静态语义:

  • Module type annotation (M : T) is valid if the module type of M is a subtype of T. The module type of (M : T) is then T in any further type checking.
    如果 M 的模块类型是 T 的子类型,则模块类型注释 (M : T) 有效。在任何进一步的类型检查中, (M : T) 的模块类型都是 T

  • Module type S is a subtype of T if the set of definitions in S is a superset of those in T. Definitions in T are permitted to instantiate type variables from S.
    如果 S 中的定义集是 T 中的定义集的超集,则模块类型 ST 的子类型。 T 中的定义允许实例化 S 中的类型变量。

The “sub” vs. “super” in the second rule is not a typo. Consider these module types and modules:
第二条规则中的“sub”与“super”不是拼写错误。考虑这些模块类型和模块:

module type T = sig
  val a : int
end

module type S = sig
  val a : int
  val b : bool
end

module A = struct
  let a = 0
end

module AB = struct
  let a = 0
  let b = true
end

module AC = struct
  let a = 0
  let c = 'c'
end
module type T = sig val a : int end
module type S = sig val a : int val b : bool end
module A : sig val a : int end
module AB : sig val a : int val b : bool end
module AC : sig val a : int val c : char end

Module type S provides a superset of the definitions in T, because it adds a definition of b. So why is S called a subtype of T? Think about the set Type(T) of all module values M such that M : T. That set contains A, AB, AC, and many others. Also think about the set Type(S) of all module values M such that M : S. That set contains AB but not A nor AC. So Type(S)Type(T), because there are some module values that are in Type(T) but not in Type(S).
模块类型 S 提供 T 中定义的超集,因为它添加了 b 的定义。那么为什么 S 被称为 T 的子类型呢?考虑所有模块值 M 的集合 Type(T) ,使得 M : T 。该集合包含 AABAC 等。还要考虑所有模块值 M 的集合 Type(S) ,使得 M : S 。该集合包含 AB 但不包含 A 也不包含 AC 。所以 Type(S)Type(T) ,因为有一些模块值位于 Type(T) 但不在 Type(S) 中。

As another example, a module type StackHistory for stacks might customize our usual Stack signature by adding an operation history : 'a t -> int to return how many items have ever been pushed on the stack in its history. That history operation makes the set of definitions in StackHistory bigger than the set in Stack, hence the use of “superset” in the rule above. But the set of module values that implement StackHistory is smaller than the set of module values that implement Stack, hence the use of “subset”.
另一个例子,堆栈的模块类型 StackHistory 可以通过添加操作 history : 'a t -> int 来自定义我们常用的 Stack 签名,以返回已推送到堆栈上的项目数。堆栈在其历史中。 history 操作使 StackHistory 中的定义集大于 Stack 中的定义集,因此在上面的规则中使用“超集”。但是实现 StackHistory 的模块值集合小于实现 Stack 的模块值集合,因此使用“子集”。

5.2.5. Module Types are Static
5.2.5. 模块类型是静态的 ¶

Decisions about validity of module type annotations are made at compile time rather than run time.
关于模块类型注释有效性的决定是在编译时而不是运行时做出的。

Important 重要的

Module type annotations therefore offer potential confusion to programmers accustomed to object-oriented languages, in which subtyping works differently.
因此,模块类型注释给习惯于面向对象语言的程序员带来了潜在的困惑,在面向对象语言中,子类型的工作方式不同。

Python programmers, for example, are accustomed to so-called “duck typing”. They might expect ((M : X) : Z) to be valid, because z does exist at run-time in M. But in OCaml, the compile-time type of (M : X) has hidden z from view irrevocably.
例如,Python 程序员习惯于所谓的“鸭子打字”。他们可能期望 ((M : X) : Z) 有效,因为 z 在运行时确实存在于 M 中。但在 OCaml 中, (M : X) 的编译时类型已不可撤销地隐藏了 z

Java programmers, on the other hand, might expect that module type annotations work like type casts. So it might seem valid to first “cast” M to X then to Z. In Java such type casts are checked, as needed, at run time. But OCaml module type annotations are static. Once an annotation of X is made, there is no way to check at compile time what other items might exist in the module—that would require a run-time check, which OCaml does not permit.
另一方面,Java 程序员可能期望模块类型注释像类型转换一样工作。因此,首先将 M “转换”为 X 然后再转换为 Z 似乎是有效的。在 Java 中,此类类型转换会根据需要在运行时进行检查。但 OCaml 模块类型注释是静态的。一旦进行了 X 注释,就无法在编译时检查模块中可能存在哪些其他项,这需要运行时检查,而 OCaml 不允许这样做。

In both cases it might feel as though OCaml is being too restrictive. Maybe. But in return for that restrictiveness, OCaml is guaranteeing an absence of run-time errors of the kind that would occur in Java or Python, whether because of a run-time error from a cast, or a run-time error from a missing method.
在这两种情况下,您可能会感觉 OCaml 限制过于严格。或许。但作为这种限制性的回报,OCaml 保证不会出现 Java 或 Python 中出现的运行时错误,无论是由于强制转换导致的运行时错误,还是由于缺少方法而导致的运行时错误。

5.2.6. First-Class Modules
5.2.6. 第一阶级的模块 ¶

Modules are not as first-class in OCaml as functions. But it is possible to package modules as first-class values. Briefly:
在 OCaml 中,模块不像函数那样具有第一阶级的地位。但可以将模块打包为第一阶级的值。简要地:

  • (module M : T) packages module M with module type T into a value.
    (module M : T) 将模块类型 T 的模块 M 打包为一个值。

  • (val e : T) un-packages e into a module with type T.
    (val e : T)e 解包到类型为 T 的模块中。

We won’t cover this much further, but if you’re curious you can have a look at the manual.
我们不会进一步介绍这一点,但如果您好奇,可以查看手册。

5.3. Modules and the Toplevel
5.3. 模块和顶层 ¶

Note 笔记

The video below uses the legacy build system, ocamlbuild, rather than the new build system, dune. Some of the details change with dune, as described in the text below.
下面的视频使用旧版构建系统 ocamlbuild,而不是新的构建系统 Dune (沙丘)。一些细节会随着沙丘的变化而变化,如下文所述。

There are several pragmatics involving modules and the toplevel that are important to master to use the two together effectively.
有一些涉及模块和顶层的实用知识,掌握这些实用知识对于有效地结合使用两者非常重要。

5.3.1. Loading Compiled Modules
5.3.1. 加载编译模块 ¶

Compiling an OCaml file produces a module having the same name as the file, but with the first letter capitalized. These compiled modules can be loaded into the toplevel using #load.
编译 OCaml 文件会生成一个与该文件同名的模块,但首字母大写。这些已编译的模块可以使用 #load 加载到顶层。

For example, suppose you create a file called mods.ml, and put the following code in it:
例如,假设您创建一个名为 mods.ml 的文件,并将以下代码放入其中:

let b = "bigred"
let inc x = x + 1
module M = struct
  let y = 42
end

Note that there is no module Mods = struct ... end around that. The code is at the topmost level of the file, as it were.
请注意,周围没有 module Mods = struct ... end 。可以说,代码位于文件的最顶层。

Then suppose you type ocamlc mods.ml to compile it. One of the newly-created files is mods.cmo: this is a compiled module object file, aka bytecode.
然后假设您输入 ocamlc mods.ml 来编译它。新创建的文件之一是 mods.cmo :这是一个已编译的模块目标文件,也称为字节码。

You can make this bytecode available for use in the toplevel with the following directives. Recall that the # character is required in front of a directive. It is not part of the prompt.
您可以使用以下指令使该字节码可在顶层使用。回想一下,指令前面需要 # 字符。它不是提示的一部分。

# #load "mods.cmo";;

That directive loads the bytecode found in mods.cmo, thus making a module named Mods available to be used. It is exactly as if you had entered this code:
该指令加载 mods.cmo 中找到的字节码,从而使名为 Mods 的模块可供使用。就好像您输入了以下代码:

module Mods = struct
  let b = "bigred"
  let inc x = x + 1
  module M = struct
    let y = 42
  end
end
module Mods :
  sig val b : string val inc : int -> int module M : sig val y : int end end

Both of these expressions will therefore evaluate successfully:
因此,这两个表达式都将成功求值:

Mods.b;;
Mods.M.y;;
- : string = "bigred"
- : int = 42

But this will fail:
但这会失败:

inc
File "[3]", line 1, characters 0-3:
1 | inc
    ^^^
Error: Unbound value inc
Hint: Did you mean incr?

It fails because inc is in the namespace of Mods.
它失败是因为 inc 位于 Mods 的命名空间中。

Mods.inc
- : int -> int = <fun>

Of course, if you open the module, you can directly name inc:
当然,如果打开模块,可以直接命名 inc

open Mods;;
inc;;
- : int -> int = <fun>

5.3.2. Dune 5.3.2. 沙丘 ¶

Dune provides a command to make it easier to start utop with libraries already loaded. Suppose we add this dune file to the same directory as mods.ml:
Dune 提供了一个命令,可以更轻松地在已加载的库的情况下启动 utop。假设我们将此沙丘文件添加到与 mods.ml 相同的目录中:

(library
 (name mods))

That tells dune to build a library named Mods out of mods.ml (and any other files in the same directory, if they existed). Then we can run this command to launch utop with that library already loaded:
这告诉dune从 mods.ml (以及同一目录中的任何其他文件,如果存在)构建一个名为 Mods 的库。然后我们可以运行此命令来启动已加载该库的 utop:

$ dune utop

Now right away we can access components of Mods without having to issue a #load directive:
现在我们可以立即访问 Mods 的组件,而无需发出 #load 指令:

Mods.inc
- : int -> int = <fun>

The dune utop command accepts a directory name as an argument if you want to load libraries in a particular subdirectory of your source code.
如果您想要加载源代码的特定子目录中的库, dune utop 命令接受目录名称作为参数。

5.3.3. Initializing the Toplevel
5.3.3. 初始化顶层 ¶

If you are doing a lot of testing of a particular module, it can be annoying to have to type directives every time you start utop. You really want to initialize the toplevel with some code as it launches, so that you don’t have to keep typing that code.
如果您正在对某个特定模块进行大量测试,那么每次启动 utop 时都必须输入指令可能会很烦人。您确实希望在顶层启动时使用一些代码对其进行初始化,这样您就不必继续键入该代码。

The solution is to create a file in the working directory and call that file .ocamlinit. Note that the . at the front of that filename is required and makes it a hidden file that won’t appear in directory listings unless explicitly requested (e.g., with ls -a). Everything in .ocamlinit will be processed by utop when it loads.
解决方案是在工作目录中创建一个文件并调用该文件 .ocamlinit 。请注意,该文件名前面的 . 是必需的,并且使其成为隐藏文件,除非明确请求(例如,使用 ls -a ),否则不会出现在目录列表中。 .ocamlinit 中的所有内容都会在加载时由 utop 处理。

For example, suppose you create a file named .ocamlinit in the same directory as mods.ml, and in that file put the following code:
例如,假设您在与 mods.ml 相同的目录中创建一个名为 .ocamlinit 的文件,并在该文件中添加以下代码:

open Mods;;

Now restart utop with dune utop. All the names defined in Mods will already be in scope. For example, these will both succeed:
现在使用 dune utop 重新启动 utop 。 Mods 中定义的所有名称都已在范围内。例如,这些都会成功:

inc;;
M.y;;
- : int -> int = <fun>
- : int = 42

5.3.4. Requiring Libraries
5.3.4. 请求库 ¶

Suppose you wanted to experiment with some OUnit code in utop. You can’t actually open it:
假设您想在 utop 中试验一些 OUnit 代码。你实际上无法打开它:

open OUnit2;;
File "[8]", line 1, characters 5-11:
1 | open OUnit2;;
         ^^^^^^
Error: Unbound module OUnit2
Hint: Did you mean Unit?

The problem is that the OUnit library hasn’t been loaded into utop yet. It can be with the following directive:
问题是 OUnit 库还没有加载到 utop 中。它可以与以下指令一起使用:

#require "ounit2";;

Now you can successfully load your own module without getting an error.
现在您可以成功加载自己的模块而不会出现错误。

open OUnit2;;

5.3.5. Load vs Use
5.3.5. 载入 vs 导入 ¶

There is a big difference between #load-ing a compiled module file and #use-ing an uncompiled source file. The former loads bytecode and makes it available for use. For example, loading mods.cmo caused the Mod module to be available, and we could access its members with expressions like Mod.b. The latter (#use) is textual inclusion: it’s like typing the contents of the file directly into the toplevel. So using mods.ml does not cause a Mod module to be available, and the definitions in the file can be accessed directly, e.g., b.
#load 加载的已编译的模块文件和用 #use 导入的未编译的源文件之间存在很大差异。前者加载字节码并使其可供使用。例如,加载 mods.cmo 导致 Mod 模块可用,我们可以使用 Mod.b 等表达式访问其成员。后者( #use )是文本包含:就像将文件的内容直接输入到顶层一样。因此使用 mods.ml 不会导致 Mod 模块可用,并且可以直接访问文件中的定义,例如 b

For example, in the following interaction, we can directly refer to b but cannot use the qualified name Mods.b:
例如,在下面的交互中,我们可以直接引用 b 但不能使用限定名 Mods.b

# #use "mods.ml"

# b;;
val b : string = "bigred"

# Mods.b;;
Error: Unbound module Mods

Whereas in this interaction the situation is reversed:
而在这种互动中,情况正好相反:

# #directory "_build";;
# #load "mods.cmo";;

# Mods.b;;
- : string = "bigred"

# b;;
Error: Unbound value b

So when you’re using the toplevel to experiment with your code, it’s often better to work with #load rather than #use. The #load directive accurately reflects how your modules interact with each other and with the outside world.
因此,当您使用顶层来试验代码时,通常使用 #load 而不是 #use 更好。 #load 指令准确地反映了模块之间以及与外部世界的交互方式。

5.4. Encapsulation 5.4. 封装 ¶

One of the main concerns of a module system is to provide encapsulation: the hiding of information about implementation behind an interface. OCaml’s module system makes this possible with a feature we’ve already seen: the opacity that module type annotations create. One special use of opacity is the declaration of abstract types. We’ll study both of those ideas in this section.
模块系统的主要关注点之一是提供封装:将有关实现的信息隐藏在接口后面。 OCaml 的模块系统通过我们已经看到的一个功能使这成为可能:模块类型注释创建的不透明度。不透明度的一种特殊用途是抽象类型的声明。我们将在本节中研究这两个想法。

5.4.1. Opacity 5.4.1. 不透明度 ¶

When implementing a module, you might sometimes have helper functions that you don’t want to expose to clients of the module. For example, maybe you’re implementing a math module that provides a tail-recursive factorial function:
实现模块时,有时您可能不希望向模块的客户端公开辅助函数。例如,也许您正在实现一个提供尾递归阶乘函数的数学模块:

module Math = struct
  (** [fact_aux n acc] is [n! * acc]. *)
  let rec fact_aux n acc =
    if n = 0 then acc else fact_aux (n - 1) (n * acc)

  (** [fact n] is [n!]. *)
  let fact n = fact_aux n 1
end
module Math : sig val fact_aux : int -> int -> int val fact : int -> int end

You’d like to make fact usable by clients of Math, but you’d also like to keep fact_aux hidden. But in the code above, you can see that fact_aux is visible in the signature inferred for Math. One way to hide it is simply to nest fact_aux:
您希望使 fact 可供 Math 的客户端使用,但您还希望保持 fact_aux 隐藏。但在上面的代码中,您可以看到 fact_aux 在为 Math 推断的签名中可见。隐藏它的一种方法是简单地嵌套 fact_aux

module Math = struct
  (** [fact n] is [n!]. *)
  let fact n =
    (** [fact_aux n acc] is [n! * acc]. *)
    let rec fact_aux n acc =
      if n = 0 then acc else fact_aux (n - 1) (n * acc)
    in
    fact_aux n 1
end
module Math : sig val fact : int -> int end

Look at the signature, and notice how fact_aux is gone. But, that nesting makes fact just a little harder to read. It also means fact_aux is not available for any other functions inside Math to use. In this case that’s probably fine—there probably aren’t any other functions in Math that need fact_aux. But if there were, we couldn’t nest fact_aux.
查看签名,注意 fact_aux 是如何消失的。但是,这种嵌套使得 fact 有点难以阅读。这也意味着 fact_aux 不可供 Math 内的任何其他函数使用。在这种情况下,这可能没问题 - Math 中可能没有任何其他函数需要 fact_aux 。但如果有的话,我们就无法嵌套 fact_aux

So another way to hide fact_aux from clients of Math, while still leaving it available for implementers of Math, is to use a module type that exposes only those names that clients should see:
因此,从 Math 的客户端隐藏 fact_aux 的另一种方法是使用仅公开这些名称的模块类型,同时仍将其保留给 Math 的实现者使用客户应该看到:

module type MATH = sig
  (** [fact n] is [n!]. *)
  val fact : int -> int
end

module Math : MATH = struct
  (** [fact_aux n acc] is [n! * acc]. *)
  let rec fact_aux n acc =
    if n = 0 then acc else fact_aux (n - 1) (n * acc)

  let fact n = fact_aux n 1
end
module type MATH = sig val fact : int -> int end
module Math : MATH

Now since MATH does not mention fact_aux, the module type annotation Math : MATH causes fact_aux to be hidden:
现在,由于 MATH 没有提及 fact_aux ,模块类型注释 Math : MATH 导致 fact_aux 被隐藏:

Math.fact_aux
File "[4]", line 1, characters 0-13:
1 | Math.fact_aux
    ^^^^^^^^^^^^^
Error: Unbound value Math.fact_aux

In that sense, module type annotations are opaque: they can prevent visibility of module items. We say that the module type seals the module, making any components not named in the module type be inaccessible.
从这个意义上说,模块类型注释是不透明的:它们可以防止模块项的可见性。我们说模块类型密封了模块,使得任何未在模块类型中命名的组件都无法访问。

Important 重要的

Remember that module type annotations are therefore not only about checking to see whether a module defines certain items. The annotations also hide items.
请记住,模块类型注释不仅仅用于检查模块是否定义了某些项目。注释还隐藏项目。

What if you did want to just check the definitions, but not hide anything? Then don’t supply the annotation at the time of module definition:
如果您只想检查定义而不隐藏任何内容怎么办?然后在模块定义时不要提供注释:

module type MATH = sig
  (** [fact n] is [n!]. *)
  val fact : int -> int
end

module Math = struct
  (** [fact_aux n acc] is [n! * acc]. *)
  let rec fact_aux n acc =
    if n = 0 then acc else fact_aux (n - 1) (n * acc)

  let fact n = fact_aux n 1
end

module MathCheck : MATH = Math
module type MATH = sig val fact : int -> int end
module Math : sig val fact_aux : int -> int -> int val fact : int -> int end
module MathCheck : MATH

Now Math.fact_aux is visible, but MathCheck.fact_aux is not:
现在 Math.fact_aux 可见,但 MathCheck.fact_aux 不可见:

Math.fact_aux
- : int -> int -> int = <fun>
MathCheck.fact_aux
File "[7]", line 1, characters 0-18:
1 | MathCheck.fact_aux
    ^^^^^^^^^^^^^^^^^^
Error: Unbound value MathCheck.fact_aux

You wouldn’t even have to give the “check” module a name since you probably never intend to access it; you could instead leave it anonymous:
您甚至不必为“检查”模块命名,因为您可能从来不打算访问它;您可以将其保留为匿名:

module _ : MATH = Math

A Comparison to Visibility Modifiers. The use of sealing in OCaml is thus similar to the use of visibility modifiers such as private and public in Java. In fact one way to think about Java class definitions is that they simultaneously define multiple signatures.
与可见性修饰符的比较。因此,OCaml 中密封的使用类似于 Java 中 privatepublic 等可见性修饰符的使用。事实上,考虑 Java 类定义的一种方法是它们同时定义多个签名。

For example, consider this Java class:
例如,考虑这个 Java 类:

class C {
  private int x;
  public int y;
}

An analogy to it in OCaml would be the following modules and types:
OCaml 中的类比是以下模块和类型:

module type C_PUBLIC = sig
  val y : int
end

module CPrivate = struct
  let x = 0
  let y = 0
end

module C : C_PUBLIC = CPrivate
module type C_PUBLIC = sig val y : int end
module CPrivate : sig val x : int val y : int end
module C : C_PUBLIC

With those definitions, any code that uses C will have access only to the names exposed in the C_PUBLIC module type.
通过这些定义,任何使用 C 的代码都只能访问 C_PUBLIC 模块类型中公开的名称。

That analogy can be extended to the other visibility modifiers, protected and default, as well. Which means that Java classes are effectively defining four related types, and the compiler is making sure the right type is used at each place in the code base C is named. No wonder it can be challenging to master visibility in OO languages at first.
这个类比也可以扩展到其他可见性修饰符, protected 和默认值。这意味着 Java 类有效地定义了四种相关类型,并且编译器确保在命名的代码库 C 中的每个位置都使用正确的类型。难怪一开始掌握面向对象语言的可见性会很困难。

5.4.2. Abstract Types 5.4.2. 抽象类型 ¶

In an earlier section we implemented stacks as lists with the following module and type:
在前面的部分中,我们将堆栈实现为具有以下模块和类型的列表:

module type LIST_STACK = sig
  (** [Empty] is raised when an operation cannot be applied
      to an empty stack. *)
  exception Empty

  (** [empty] is the empty stack. *)
  val empty : 'a list

  (** [is_empty s] is whether [s] is empty. *)
  val is_empty : 'a list -> bool

  (** [push x s] pushes [x] onto the top of [s]. *)
  val push : 'a -> 'a list -> 'a list

  (** [peek s] is the top element of [s].
      Raises [Empty] if [s] is empty. *)
  val peek : 'a list -> 'a

  (** [pop s] is all but the top element of [s].
      Raises [Empty] if [s] is empty. *)
  val pop : 'a list -> 'a list
end

module ListStack : LIST_STACK = struct
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
end
module type LIST_STACK =
  sig
    exception Empty
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end
module ListStack : LIST_STACK

What if we wanted to modify that data structure to add an operation for the size of the stack? The easy way would be to implement it using List.length:
如果我们想修改该数据结构以添加针对堆栈大小的操作怎么办?最简单的方法是使用 List.length 来实现它:

module type LIST_STACK = sig
  ...
  (** [size s] is the number of elements on the stack. *)
  val size : 'a list -> int
end

module ListStack : LIST_STACK = struct
  ...
  let size = List.length
end

That results in a linear-time implementation of size. What if we wanted a faster, constant-time implementation? At the cost of a little space, we could cache the size of the stack. Let’s now represent the stack as a pair, where the first component of the pair is the same list as before, and the second component of the pair is the size of the stack:
这导致 size 的线性时间实现。如果我们想要更快、恒定时间的实现怎么办?以一点空间为代价,我们可以缓存堆栈的大小。现在让我们将堆栈表示为一对,其中该对的第一个组件是与之前相同的列表,该对的第二个组件是堆栈的大小:

module ListStackCachedSize = struct
  exception Empty
  let empty = ([], 0)
  let is_empty = function ([], _) -> true | _ -> false
  let push x (stack, size) = (x :: stack, size + 1)
  let peek = function ([], _) -> raise Empty | (x :: _, _) -> x
  let pop = function
    | ([], _) -> raise Empty
    | (_ :: stack, size) -> (stack, size - 1)
end
module ListStackCachedSize :
  sig
    exception Empty
    val empty : 'a list * int
    val is_empty : 'a list * 'b -> bool
    val push : 'a -> 'a list * int -> 'a list * int
    val peek : 'a list * 'b -> 'a
    val pop : 'a list * int -> 'a list * int
  end

We have a big problem. ListStackCachedSize does not implement the LIST_STACK module type, because that module type specifies 'a list throughout it to represent the stack—not 'a list * int.
我们有一个大问题。 ListStackCachedSize 不实现 LIST_STACK 模块类型,因为该模块类型在整个模块类型中指定 'a list 来表示堆栈,而不是 'a list * int

module CheckListStackCachedSize : LIST_STACK = ListStackCachedSize
File "[12]", line 1, characters 47-66:
1 | module CheckListStackCachedSize : LIST_STACK = ListStackCachedSize
                                                   ^^^^^^^^^^^^^^^^^^^
Error: Signature mismatch:
       ...
       Values do not match:
         val empty : 'a list * int
       is not included in
         val empty : 'a list
       The type 'a list * int is not compatible with the type 'b list
       File "[10]", line 7, characters 2-21: Expected declaration
       File "[11]", line 3, characters 6-11: Actual declaration

Moreover, any code we previously wrote using ListStack now has to be modified to deal with the pair, which could mean revising pattern matches, function types, and so forth.
此外,我们以前使用 ListStack 编写的任何代码现在都必须进行修改才能处理该对,这可能意味着修改模式匹配、函数类型等。

As you no doubt learned in earlier programming courses, the problem we are encountering here is a lack of encapsulation. We should have kept the type that implements ListStack hidden from clients. In Java, for example, we might have written:
毫无疑问,正如您在早期的编程课程中了解到的那样,我们在这里遇到的问题是缺乏封装。我们应该对客户端隐藏实现 ListStack 的类型。例如,在 Java 中,我们可能会这样写:

class ListStack<T> {
  private List<T> stack;
  private int size;
  ...
}

That way clients of ListStack would be unaware of stack or size. In fact, they wouldn’t be able to name those fields at all. Instead, they would just use ListStack as the type of the stack:
这样 ListStack 的客户端将不知道 stacksize 。事实上,他们根本无法命名这些字段。相反,他们只会使用 ListStack 作为堆栈的类​​型:

ListStack<Integer> s = new ListStack<>();
s.push(1);

So in OCaml, how can we keep the representation type of the stack hidden? What we learned about opacity and sealing thus far does not suffice. The problem is that the type 'a list * int literally appears in the signature of ListStackCachedSize, e.g. in push:
那么在OCaml中,我们如何才能隐藏堆栈的表示类型呢?到目前为止,我们对不透明度和密封的了解还不够。问题在于 'a list * int 类型确实出现在 ListStackCachedSize 的签名中,例如在 push 中:

ListStackCachedSize.push
- : 'a -> 'a list * int -> 'a list * int = <fun>

A module type annotation could hide one of the values defined in ListStackCachedSize, e.g., push itself, but that doesn’t solve the problem: we need to hide the type 'a list * int while exposing the operation push. So OCaml has a feature for doing exactly that: abstract types. Let’s see an example of this feature.
模块类型注释可以隐藏 ListStackCachedSize 中定义的值之一,例如 push 本身,但这并不能解决问题:我们需要隐藏类型 'a list * int 。因此 OCaml 有一个功能可以做到这一点:抽象类型。让我们看一下此功能的示例。

We begin by modifying LIST_STACK, replacing 'a list with a new type 'a stack everywhere. We won’t repeat the specification comments here, so as to keep the example shorter. And while we’re at it, let’s add the size operation.
我们首先修改 LIST_STACK ,将 'a list 替换为新类型 'a stack 。我们不会在这里重复规范注释,以便使示例更简短。当我们这样做时,让我们添加 size 操作。

module type LIST_STACK = sig
  type 'a stack
  exception Empty
  val empty : 'a stack
  val is_empty : 'a stack -> bool
  val push : 'a -> 'a stack -> 'a stack
  val peek : 'a stack -> 'a
  val pop : 'a stack -> 'a stack
  val size : 'a stack -> int
end
module type LIST_STACK =
  sig
    type 'a stack
    exception Empty
    val empty : 'a stack
    val is_empty : 'a stack -> bool
    val push : 'a -> 'a stack -> 'a stack
    val peek : 'a stack -> 'a
    val pop : 'a stack -> 'a stack
    val size : 'a stack -> int
  end

Note how 'a stack is not actually defined in that signature. We haven’t said anything about what it is. It might be 'a list, or 'a list * int, or {stack : 'a list; size : int}, or anything else. That is what makes it an abstract type: we’ve declared its name but not specified its definition.
请注意 'a stack 实际上并未在该签名中定义。我们还没有说任何关于它是什么的事情。它可能是 'a list'a list * int{stack : 'a list; size : int} 或其他任何内容。这就是它成为抽象类型的原因:我们声明了它的名称,但没有指定它的定义。

Now ListStackCachedSize can implement that module type with the addition of just one line of code: the first line of the structure, which defines 'a stack:
现在 ListStackCachedSize 可以通过添加一行代码来实现该模块类型:结构体的第一行,它定义 'a stack

module ListStackCachedSize : LIST_STACK = struct
  type 'a stack = 'a list * int
  exception Empty
  let empty = ([], 0)
  let is_empty = function ([], _) -> true | _ -> false
  let push x (stack, size) = (x :: stack, size + 1)
  let peek = function ([], _) -> raise Empty | (x :: _, _) -> x
  let pop = function
    | ([], _) -> raise Empty
    | (_ :: stack, size) -> (stack, size - 1)
  let size = snd
end
module ListStackCachedSize : LIST_STACK

Take a careful look at the output: nowhere does 'a list show up in it. In fact, only LIST_STACK does. And LIST_STACK mentions only 'a stack. So no one’s going to know that internally a list is used. (Ok, they’re going to know: the name suggests it. But the point is they can’t take advantage of that, because the type is abstract.)
仔细查看输出:其中没有 'a list 出现。事实上,只有 LIST_STACK 可以。并且 LIST_STACK 仅提及 'a stack 。所以没有人会知道内部使用了列表。 (好吧,他们会知道:顾名思义。但关键是他们无法利用这一点,因为类型是抽象的。)

Likewise, our original implementation with linear-time size satisfies the module type. We just have to add a line to define 'a stack:
同样,我们最初的线性时间 size 实现满足模块类型。我们只需要添加一行来定义 'a stack

module ListStack : LIST_STACK = struct
  type 'a stack = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
end
module ListStack : LIST_STACK

Note that omitting that added line would result in an error, just as if we had failed to define push or any of the other operations from the module type:
请注意,省略添加的行将导致错误,就像我们未能定义 push 或模块类型中的任何其他操作一样:

module ListStack : LIST_STACK = struct
  (* type 'a stack = 'a list *)
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
end
File "[17]", lines 1-10, characters 32-3:
 1 | ................................struct
 2 |   (* type 'a stack = 'a list *)
 3 |   exception Empty
 4 |   let empty = []
 5 |   let is_empty = function [] -> true | _ -> false
 6 |   let push x s = x :: s
 7 |   let peek = function [] -> raise Empty | x :: _ -> x
 8 |   let pop = function [] -> raise Empty | _ :: s -> s
 9 |   let size = List.length
10 | end
Error: Signature mismatch:
       ...
       The type `stack' is required but not provided
       File "[14]", line 2, characters 2-15: Expected declaration

Here is a third, custom implementation of LIST_STACK. This one is deliberately overly-complicated, in part to illustrate how the abstract type can hide implementation details that are better not revealed to clients:
这是 LIST_STACK 的第三个自定义实现。这个故意过于复杂,部分是为了说明抽象类型如何隐藏最好不要向客户端透露的实现细节:

module CustomStack : LIST_STACK = struct
  type 'a entry = {top : 'a; rest : 'a stack; size : int}
  and 'a stack = S of 'a entry option
  exception Empty
  let empty = S None
  let is_empty = function S None -> true | _ -> false
  let size = function S None -> 0 | S (Some {size}) -> size
  let push x s = S (Some {top = x; rest = s; size = size s + 1})
  let peek = function S None -> raise Empty | S (Some {top}) -> top
  let pop = function S None -> raise Empty | S (Some {rest}) -> rest
end
module CustomStack : LIST_STACK

Is that really a “list” stack? It satisfies the module type LIST_STACK. But upon reflection, that module type never really had anything to do with lists once we made the type 'a stack abstract. There’s really no need to call it LIST_STACK. We’d be better off using just STACK, since it can be implemented with list or without. At that point, we could just go with Stack as its name, since there is no module named Stack we’ve written that would be confused with it. That avoids the all-caps look of our code shouting at us.
这真的是一个“列表”堆栈吗?它满足模块类型 LIST_STACK 。但经过反思,一旦我们将类型 'a stack 抽象化,该模块类型就从未真正与列表有任何关系。确实没有必要将其称为 LIST_STACK 。我们最好只使用 STACK ,因为它可以使用 list 来实现,也可以不使用 list 来实现。此时,我们可以使用 Stack 作为其名称,因为我们编写的名为 Stack 的模块不会与它混淆。这避免了代码全大写的样子对我们大喊大叫。

module type Stack = sig
  type 'a stack
  exception Empty
  val empty : 'a stack
  val is_empty : 'a stack -> bool
  val push : 'a -> 'a stack -> 'a stack
  val peek : 'a stack -> 'a
  val pop : 'a stack -> 'a stack
  val size : 'a stack -> int
end

module ListStack : Stack = struct
  type 'a stack = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
end
module type Stack =
  sig
    type 'a stack
    exception Empty
    val empty : 'a stack
    val is_empty : 'a stack -> bool
    val push : 'a -> 'a stack -> 'a stack
    val peek : 'a stack -> 'a
    val pop : 'a stack -> 'a stack
    val size : 'a stack -> int
  end
module ListStack : Stack

There’s one further naming improvement we could make. Notice the type of ListStack.empty (and don’t worry about the abstr part for now; we’ll come back to it):
我们还可以进一步改进命名。注意 ListStack.empty 的类型(现在不用担心 abstr 部分;我们稍后会再讨论它):

ListStack.empty
- : 'a ListStack.stack = <abstr>

That type, 'a ListStack.stack, is rather unwieldy, because it conveys the word “stack” twice: once in the name of the module, and again in the name of the representation type inside that module. In places like this, OCaml programmers idiomatically use a standard name, t, in place of a longer representation type name:
该类型 'a ListStack.stack 相当笨重,因为它两次传达“stack”一词:一次在模块名称中,另一次在该模块内的表示类型名称中。在这样的地方,OCaml 程序员惯用使用标准名称 t 来代替较长的表示类型名称:

module type Stack = sig
  type 'a t
  exception Empty
  val empty : 'a t
  val is_empty : 'a t -> bool
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a
  val pop : 'a t -> 'a t
  val size : 'a t -> int
end

module ListStack : Stack = struct
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
end

module CustomStack : Stack = struct
  type 'a entry = {top : 'a; rest : 'a t; size : int}
  and 'a t = S of 'a entry option
  exception Empty
  let empty = S None
  let is_empty = function S None -> true | _ -> false
  let size = function S None -> 0 | S (Some {size}) -> size
  let push x s = S (Some {top = x; rest = s; size = size s + 1})
  let peek = function S None -> raise Empty | S (Some {top}) -> top
  let pop = function S None -> raise Empty | S (Some {rest}) -> rest
end
module type Stack =
  sig
    type 'a t
    exception Empty
    val empty : 'a t
    val is_empty : 'a t -> bool
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val pop : 'a t -> 'a t
    val size : 'a t -> int
  end
module ListStack : Stack
module CustomStack : Stack

Now the type of stacks is simpler:
现在堆栈的类型更简单:

ListStack.empty;;
CustomStack.empty;;
- : 'a ListStack.t = <abstr>
- : 'a CustomStack.t = <abstr>

That idiom is fairly common when there’s a single representation type exposed by an interface to a data structure. You’ll see it used throughout the standard library.
当数据结构的接口公开单一表示类型时,这种习惯用法相当常见。您将在整个标准库中看到它的使用。

In informal conversation we would usually pronounce those types without the “dot t” part. For example, we might say “alpha ListStack”, simply ignoring the t—though it does technically have to be there to be legal OCaml code.
在非正式对话中,我们通常会在发音时不带“点t”部分。例如,我们可能会说“alpha ListStack”,只是忽略 t — 尽管从技术上讲它必须存在才能成为合法的 OCaml 代码。

Finally, abstract types are really just a special case of opacity. You actually can expose the definition of a type in a signature if you want to:
最后,抽象类型实际上只是不透明的一种特殊情况。如果您愿意,实际上可以在签名中公开类型的定义:

module type T = sig
  type t = int
  val x : t
end

module M : T = struct
  type t = int
  let x = 42
end

let a : int = M.x
module type T = sig type t = int val x : t end
module M : T
val a : int = 42

Note how we’re able to use M.x at its type of int. That works because the equality of types t and int has been exposed in the module type. But if we kept t abstract, the same usage would fail:
请注意我们如何能够以 int 类型使用 M.x 。这是可行的,因为类型 tint 的相等性已在模块类型中公开。但如果我们保持 t 抽象,相同的用法将会失败:

module type T = sig
  type t (* = int *)
  val x : t
end

module M : T = struct
  type t = int
  let x = 42
end

let a : int = M.x
module type T = sig type t val x : t end
module M : T
File "[24]", line 11, characters 14-17:
11 | let a : int = M.x
                   ^^^
Error: This expression has type M.t but an expression was expected of type
         int

We’re not allowed to use M.x at type int outside of M, because its type M.t is abstract. This is encapsulation at work, keeping that implementation detail hidden.
我们不允许在 M 之外的 int 类型上使用 M.x ,因为它的类型 M.t 是抽象的。这是工作中的封装,隐藏了实现细节。

5.4.3. Pretty Printing 5.4.3. 漂亮的打印 ¶

In some output above, we observed something curious: the toplevel prints <abstr> in place of the actual contents of a value whose type is abstract:
在上面的一些输出中,我们观察到一些奇怪的事情:顶层打印 <abstr> 代替抽象类型值的实际内容:

ListStack.empty;;
ListStack.(empty |> push 1 |> push 2);;
- : 'a ListStack.t = <abstr>
- : int ListStack.t = <abstr>

Recall that the toplevel uses this angle-bracket convention to indicate an unprintable value. We’ve encountered that before with functions and <fun>:
回想一下,顶层使用这个尖括号约定来指示不可打印的值。我们之前在函数和 <fun> 中遇到过这种情况:

fun x -> x
- : 'a -> 'a = <fun>

On the one hand, it’s reasonable for the toplevel to behave this way. Once a type is abstract its implementation isn’t meant to be revealed to clients. So actually printing out the list [] or [2; 1] as responses to the above inputs would be revealing more than is intended.
一方面,高层的这种做法是合理的。一旦类型是抽象的,它的实现就不会透露给客户端。因此,实际打印列表 [][2; 1] 作为对上述输入的响应将揭示比预期更多的信息。

On the other hand, it’s also reasonable for implementers to provide clients with a friendly way to view a value of an abstract type. Java programmers, for example, will often write toString() methods so that objects can be printed as output in the terminal or in JShell. To support that, the OCaml toplevel has a directive #install_printer, which registers a function to print values. Here’s how it works.
另一方面,实现者为客户提供一种友好的方式来查看抽象类型的值也是合理的。例如,Java 程序员经常编写 toString() 方法,以便可以将对象作为输出打印在终端或 JShell 中。为了支持这一点,OCaml 顶层有一个指令 #install_printer ,它注册一个函数来打印值。这是它的工作原理。

  • You write a pretty printing function of type Format.formatter -> t -> unit, for whatever type t you like. Let’s suppose for sake of example that you name that function pp.
    您可以为您喜欢的任何类型 t 编写一个漂亮的 Format.formatter -> t -> unit 类型打印函数。举例来说,假设您将该函数命名为 pp

  • You invoke #install_printer pp in the toplevel.
    您在顶层调用 #install_printer pp

  • From now on, anytime the toplevel wants to print a value of type t it uses your function pp to do so.
    从现在开始,只要顶层想要打印 t 类型的值,它就会使用您的函数 pp 来执行此操作。

It probably makes sense the pretty printing function needs to take in a value of type t (because that’s what it needs to print) and returns unit (as other printing functions do). But why does it take the Format.formatter argument? It’s because of a fairly high-powered feature that OCaml is attempting to provide here: automatic line breaking and indentation in the middle of very large outputs.
漂亮的打印函数需要接受 t 类型的值(因为这就是它需要打印的值)并返回 unit (与其他打印函数一样),这可能是有意义的。但为什么它需要 Format.formatter 参数呢?这是因为 OCaml 试图在这里提供一个相当强大的功能:在非常大的输出中间自动换行和缩进。

Consider the output from this expression, which creates nested lists:
考虑此表达式的输出,它创建嵌套列表:

List.init 15 (fun n -> List.init n (Fun.const n))
- : int list list =
[[]; [1]; [2; 2]; [3; 3; 3]; [4; 4; 4; 4]; [5; 5; 5; 5; 5];
 [6; 6; 6; 6; 6; 6]; [7; 7; 7; 7; 7; 7; 7]; [8; 8; 8; 8; 8; 8; 8; 8];
 [9; 9; 9; 9; 9; 9; 9; 9; 9]; [10; 10; 10; 10; 10; 10; 10; 10; 10; 10];
 [11; 11; 11; 11; 11; 11; 11; 11; 11; 11; 11];
 [12; 12; 12; 12; 12; 12; 12; 12; 12; 12; 12; 12];
 [13; 13; 13; 13; 13; 13; 13; 13; 13; 13; 13; 13; 13];
 [14; 14; 14; 14; 14; 14; 14; 14; 14; 14; 14; 14; 14; 14]]

Each inner list contains n copies of the number n. Note how the indentation and line breaks are somewhat sophisticated. All the inner lists are indented one space from the left-hand margin. Line breaks have been inserted to avoid splitting inner lists over multiple lines.
每个内部列表包含 n 个数字 n 的副本。请注意缩进和换行有些复杂。所有内部列表均从左侧边距缩进一格。已插入换行符以避免将内部列表拆分为多行。

The Format module is what provides this functionality, and Format.formatter is an abstract type in it. You could think of a formatter as being a place to send output, like a file, and have it be automatically formatted along the way. The typical use of a formatter is as argument to a function such as Format.fprintf, which like Printf uses format specifiers.
Format 模块提供了此功能,而 Format.formatter 是其中的抽象类型。您可以将格式化程序视为发送输出(如文件)的地方,并使其自动格式化。格式化程序的典型用途是作为诸如 Format.fprintf 之类的函数的参数,它与 Printf 一样使用格式说明符。

For example, suppose you wanted to change how strings are printed by the toplevel and add ” kupo” to the end of each string. Here’s code that would do it:
例如,假设您想要更改顶层打印字符串的方式,并将“kupo”添加到每个字符串的末尾。这是可以做到这一点的代码:

let kupo_pp fmt s = Format.fprintf fmt "%s kupo" s;;
#install_printer kupo_pp;;
val kupo_pp : Format.formatter -> string -> unit = <fun>

Now you can see that the toplevel adds ” kupo” to each string while printing it, even though it’s not actual a part of the original string:
现在您可以看到顶层在打印每个字符串时添加了“kupo”,即使它实际上并不是原始字符串的一部分:

let h = "Hello"
let s = String.length h
val h : string = Hello kupo
val s : int = 5

To keep ourselves from getting confused about strings in the rest of this section, let’s uninstall that pretty printer before going on:
为了避免我们对本节其余部分中的字符串感到困惑,让我们在继续之前卸载那个漂亮的打印机:

#remove_printer kupo_pp;;

As a bigger example, let’s add pretty printing to ListStack:
作为一个更大的例子,让我们为 ListStack 添加漂亮的打印:

module type Stack = sig
  type 'a t
  exception Empty
  val empty : 'a t
  val is_empty : 'a t -> bool
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a
  val pop : 'a t -> 'a t
  val size : 'a t -> int
  val pp :
    (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit
end
module type Stack =
  sig
    type 'a t
    exception Empty
    val empty : 'a t
    val is_empty : 'a t -> bool
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val pop : 'a t -> 'a t
    val size : 'a t -> int
    val pp :
      (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit
  end

First, notice that we have to expose pp as part of the module type. Otherwise it would be encapsulated, hence we wouldn’t be able to install it. Second, notice that the type of pp now takes an extra first argument of type Format.formatter -> 'a -> unit. That is itself a pretty printer for type 'a, on which t is parameterized. We need that argument in order to be able to pretty print the values of type 'a.
首先,请注意,我们必须将 pp 公开为模块类型的一部分。否则它会被封装,因此我们将无法安装它。其次,请注意 pp 的类型现在采用 Format.formatter -> 'a -> unit 类型的额外第一个参数。它本身就是一个漂亮的 'a 类型打印机,其中 t 被参数化。我们需要该参数以便能够漂亮地打印 'a 类型的值。

module ListStack : Stack = struct
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push x s = x :: s
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
  let pp pp_val fmt s =
    let open Format in
    let pp_break fmt () = fprintf fmt "@," in
    fprintf fmt "@[<v 0>top of stack";
    if s <> [] then fprintf fmt "@,";
    pp_print_list ~pp_sep:pp_break pp_val fmt s;
    fprintf fmt "@,bottom of stack@]"
end
module ListStack : Stack

In ListStack.pp, we use some of the advanced features of the Format module. Function Format.pp_print_list does the heavy lifting to print all the elements of the stack. The rest of the code handles the indentation and line breaks. Here’s the result:
ListStack.pp 中,我们使用了 Format 模块的一些高级功能。函数 Format.pp_print_list 执行繁重的工作来打印堆栈的所有元素。代码的其余部分处理缩进和换行符。结果如下:

#install_printer ListStack.pp
ListStack.empty
- : 'a ListStack.t = top of stack
                     bottom of stack
ListStack.(empty |> push 1 |> push 2)
- : int ListStack.t = top of stack
                      2
                      1
                      bottom of stack

For more information, see the toplevel manual (search for #install_printer), the Format module, and this OCaml GitHub issue. The latter seems to be the only place that documents the use of extra arguments, as in pp_val above, to print values of polymorphic types.
有关更多信息,请参阅顶级手册(搜索 #install_printer )、Format 模块和此 OCaml GitHub 问题。后者似乎是唯一记录使用额外参数(如上面的 pp_val )来打印多态类型值的地方。

5.5. Compilation Units 5.5. 编译单元 ¶

A compilation unit is a pair of OCaml source files in the same directory. They share the same base name, call it x, but their extensions differ: one file is x.ml, the other is x.mli. The file x.ml is called the implementation, and x.mli is called the interface.
编译单元是同一目录中的一对 OCaml 源文件。它们共享相同的基本名称,称为 x ,但它们的扩展名不同:一个文件是 x.ml ,另一个文件是 x.mli 。文件 x.ml 称为实现, x.mli 称为接口。

For example, suppose that foo.mli contains exactly the following:
例如,假设 foo.mli 恰好包含以下内容:

val x : int
val f : int -> int

and foo.ml, in the same directory, contains exactly the following:
foo.ml ,在同一目录中,包含以下内容:

let x = 0
let y = 12
let f x = x + y

Then compiling foo.ml will have the same effect as defining the module Foo as follows:
然后编译 foo.ml 将与定义模块 Foo 具有相同的效果,如下所示:

module Foo : sig
  val x : int
  val f : int -> int
end = struct
  let x = 0
  let y = 12
  let f x = x + y
end

In general, when the compiler encounters a compilation unit, it treats it as defining a module and a signature like this:
一般来说,当编译器遇到编译单元时,它会将其视为定义了一个模块和签名,如下所示:

module Foo
  : sig (* insert contents of foo.mli here *) end
= struct
  (* insert contents of foo.ml here *)
end

The unit name Foo is derived from the base name foo by just capitalizing the first letter. Notice that there is no named module type being defined; the signature of Foo is actually anonymous.
单元名称 Foo 源自基本名称 foo ,只需将第一个字母大写即可。请注意,没有定义命名模块类型; Foo 的签名实际上是匿名的。

The standard library uses compilation units to implement most of the modules we have been using so far, like List and String. You can see that in the standard library source code.
标准库使用编译单元来实现我们迄今为止使用的大多数模块,例如 ListString 。您可以在标准库源代码中看到这一点。

5.5.1. Documentation Comments
5.5.1. 文档注释 ¶

Some documentation comments belong in the interface file, whereas others belong in the implementation file:
一些文档注释属于接口文件,而另一些则属于实现文件:

  • Clients of an abstraction can be expected to read interface files, or rather the HTML documentation generated from them. So the comments in an interface file should be written with that audience in mind. These comments should describe how to use the abstraction, the preconditions for calling its functions, what exceptions they might raise, and perhaps some notes on what algorithms are used to implement operations. The standard library’s List module contains many examples of these kinds of comments.
    抽象的客户端可以读取接口文件,或者更确切地说是从它们生成的 HTML 文档。因此,在编写界面文件中的注释时应考虑到该受众。这些注释应该描述如何使用抽象、调用其函数的前提条件、它们可能引发哪些异常,或许还应该描述一些关于使用哪些算法来实现操作的注释。标准库的 List 模块包含许多此类注释的示例。

  • Clients should not be expected to read implementation files. Those files will be read by creators and maintainers of the implementation. The documentation in the implementation file should provide information that explains the internal details of the abstraction, such as how the representation type is used, how the code works, important internal invariants it maintains, and so forth. Maintainers can also be expected to read the specifications in the interface files.
    不应期望客户读取实施文件。这些文件将由实现的创建者和维护者读取。实现文件中的文档应该提供解释抽象内部细节的信息,例如如何使用表示类型、代码如何工作、它维护的重要内部不变量等等。维护人员还可以阅读接口文件中的规范。

Documentation should not be duplicated between the files. In particular, the client-facing specification comments in the interface file should not be duplicated in the implementation file. One reason is that duplication inevitably leads to errors. Another reason is that OCamldoc has the ability to automatically inject the comments from the interface file into the generated HTML from the implementation file.
文件之间不应重复文档。特别是,接口文件中面向客户端的规范注释不应在实现文件中重复。原因之一是重复不可避免地会导致错误。另一个原因是 OCamldoc 能够自动将接口文件中的注释注入到实现文件生成的 HTML 中。

OCamldoc comments can be placed either before or after an element of the interface. For example, both of these placements are possible:
OCamldoc 注释可以放置在界面元素之前或之后。例如,这两种放置方式都是可能的:

(** The mathematical constant 3.14... *)
val pi : float
val pi : float
(** The mathematical constant 3.14... *)

Tip

The standard library developers apparently prefer the post-placement of the comment, and OCamlFormat seems to work better with that, too.
标准库开发人员显然更喜欢放置注释,并且 OCamlFormat 似乎也能更好地处理这一点。

5.5.2. An Example with Stacks
5.5.2. 一个堆栈的例子 ¶

Put this code in mystack.mli, noting that there is no sig..end around it or any module type:
将此代码放入 mystack.mli 中,注意其周围没有 sig..end 或任何 module type

type 'a t
exception Empty
val empty : 'a t
val is_empty : 'a t -> bool
val push : 'a -> 'a t -> 'a t
val peek : 'a t -> 'a
val pop : 'a t -> 'a t

We’re using the name “mystack” because the standard library already has a Stack module. Re-using that name could lead to error messages that are somewhat hard to understand.
我们使用名称“mystack”,因为标准库已经有一个 Stack 模块。重复使用该名称可能会导致一些难以理解的错误消息。

Also put this code in mystack.ml, noting that there is no struct..end around it or any module:
还要将此代码放入 mystack.ml 中,注意其周围没有 struct..end 或任何 module

type 'a t = 'a list
exception Empty
let empty = []
let is_empty = function [] -> true | _ -> false
let push = List.cons
let peek = function [] -> raise Empty | x :: _ -> x
let pop = function [] -> raise Empty | _ :: s -> s

Create a dune file:
创建沙丘文件:

(library
 (name mystack))

Compile the code and launch utop:
编译代码并启动 utop:

$ dune utop

Your compilation unit is ready for use:
您的编译单元已可供使用:

# Mystack.empty;;
- : 'a Mystack.t = <abstr>

5.5.3. Incomplete Compilation Units
5.5.3. 不完整的编译单元 ¶

What if either the interface or implementation file is missing for a compilation unit?
如果编译单元缺少接口或实现文件怎么办?

Missing Interface Files. Actually this is exactly how we’ve normally been working up until this point. For example, you might have done some homework in a file named lab1.ml but never needed to worry about lab1.mli. There is no requirement that every .ml file have a corresponding .mli file, or in other words, that every compilation unit be complete.
缺少接口文件。事实上,这正是我们到目前为止通常的工作方式。例如,您可能已经在名为 lab1.ml 的文件中完成了一些作业,但从未需要担心 lab1.mli 。不要求每个 .ml 文件都有一个对应的 .mli 文件,或者换句话说,每个编译单元都是完整的。

If the .mli file is missing there is still a module that is created, as we saw back when we learned about #load and modules. It just doesn’t have an automatically imposed signature. For example, the situation with lab1 above would lead to the following module being created during compilation:
如果 .mli 文件丢失,仍然会创建一个模块,正如我们在了解 #load 和模块时所看到的那样。它只是没有自动强加的签名。例如,上面的 lab1 情况将导致编译期间创建以下模块:

module Lab1 = struct
  (* insert contents of lab1.ml here *)
end

Missing Implementation Files. This case is much rarer, and not one you are likely to encounter in everyday development. But be aware that there is a misuse case that Java or C++ programmers sometimes accidentally fall into. Suppose you have an interface for which there will be a few implementation. Thinking back to stacks earlier in this chapter, perhaps you have a module type Stack and two modules that implement it, ListStack and CustomStack:
缺少实施文件。这种情况比较罕见,在日常开发中不太可能遇到。但请注意,Java 或 C++ 程序员有时会无意中陷入误用情况。假设您有一个接口,其中有一些实现。回想一下本章前面的堆栈,也许您有一个模块类型 Stack 和两个实现它的模块, ListStackCustomStack

module type Stack = sig
  type 'a t
  val empty : 'a t
  val push : 'a -> 'a t -> 'a t
  (* etc. *)
end

module ListStack : Stack = struct
  type 'a t = 'a list
  let empty = []
  let push = List.cons
  (* etc. *)
end

module CustomStack : Stack = struct
  (* omitted *)
end

It’s tempting to divide that code up into files as follows:
人们很容易将该代码划分为文件,如下所示:

(********************************)
(* stack.mli *)
type 'a t
val empty : 'a t
val push : 'a -> 'a t -> 'a t
(* etc. *)

(********************************)
(* listStack.ml *)
type 'a t = 'a list
let empty = []
let push = List.cons
(* etc. *)

(********************************)
(* customStack.ml *)
(* omitted *)

The reason it’s tempting is that in Java you might put the Stack interface into a Stack.java file, the ListStack class in a ListStack.java file, and so forth. In C++ something similar might be done with .hpp and .cpp files.
它很诱人的原因是,在 Java 中,您可能会将 Stack 接口放入 Stack.java 文件中,将 ListStack 类放入 ListStack.java 文件中,等等。在 C++ 中,可以使用 .hpp.cpp 文件完成类似的操作。

But the OCaml file organization shown above just won’t work. To be a compilation unit, the interface for listStack.ml must be in listStack.mli. It can’t be in a file with any other name. So there’s no way with that code division to stipulate that ListStack : Stack.
但上面显示的 OCaml 文件组织是行不通的。要成为编译单元, listStack.ml 的接口必须位于 listStack.mli 中。它不能位于具有任何其他名称的文件中。因此,该代码划分无法规定 ListStack : Stack

Instead, the code could be divided like this:
相反,代码可以这样划分:

(********************************)
(* stack.ml *)
module type S = sig
  type 'a t
  val empty : 'a t
  val push : 'a -> 'a t -> 'a t
  (* etc. *)
end

(********************************)
(* listStack.ml *)
module M : Stack.S = struct
  type 'a t = 'a list
  let empty = []
  let push = List.cons
  (* etc. *)
end

(********************************)
(* customStack.ml *)
module M : Stack.S = struct
  (* omitted *)
end

Note the following about that division:
请注意有关该划分的以下几点:

  • The module type goes in a .ml file not a .mli, because we’re not trying to create a compilation unit.
    模块类型位于 .ml 文件中,而不是 .mli 中,因为我们并没有尝试创建编译单元。

  • We give short names to the modules and module types in the files, because they will already be inside a module based on their filename. It would be rather verbose, for example, to name S something longer like Stack. If we did, we’d have to write Stack.Stack in the module type annotations instead of Stack.S.
    我们为文件中的模块和模块类型提供短名称,因为它们已经根据其文件名位于模块内。例如,将 S 命名为更长的名称,如 Stack ,这将是相当冗长的。如果这样做,我们必须在模块类型注释中编写 Stack.Stack 而不是 Stack.S

Another possibility for code division would be to put all the code in a single file stack.ml. That works if all the code is part of the same library, but not if (e.g.) ListStack and CustomStack are developed by separate organizations. If it is in a single file, then we could turn it into a compilation unit:
代码划分的另一种可能性是将所有代码放在一个文件中 stack.ml 。如果所有代码都是同一库的一部分,则该方法有效,但如果(例如) ListStackCustomStack 由不同的组织开发,则无效。如果它在单个文件中,那么我们可以将它变成一个编译单元:

(********************************)
(* stack.mli *)
module type S = sig
  type 'a t
  val empty : 'a t
  val push : 'a -> 'a t -> 'a t
  (* etc. *)
end

module ListStack : S

module CustomStack : S

(********************************)
(* stack.ml *)
module type S = sig
  type 'a t
  val empty : 'a t
  val push : 'a -> 'a t -> 'a t
  (* etc. *)
end

module ListStack : S = struct
  type 'a t = 'a list
  let empty = []
  let push = List.cons
  (* etc. *)
end

module CustomStack : S = struct
  (* omitted *)
end

Unfortunately that does mean we’ve duplicated Stack.S in both the interface and implementation files. There’s no way to automatically “import” an already declared module type from a .mli file into the corresponding .ml file.
不幸的是,这确实意味着我们在接口和实现文件中都重复了 Stack.S 。无法自动将已声明的模块类型从 .mli 文件“导入”到相应的 .ml 文件中。

Code duplication naturally makes us unhappy. Later, with functors, we’ll see how to eliminate it.
代码重复自然会让我们不高兴。稍后,通过函子,我们将了解如何消除它。

5.6. Functional Data Structures
5.6. 函数式数据结构 ¶

A functional data structure is one that does not make use of mutability. It’s possible to build functional data structures both in functional languages and in imperative languages. For example, you could build a Java equivalent to OCaml’s list type by creating a Node class whose fields are immutable by virtue of using the const
函数式数据结构是一种不利用可变性的数据结构。可以用函数式语言和命令式语言构建函数式数据结构。例如,您可以通过创建一个 Node 类来构建与 OCaml 的 list 类型等效的 Java,该类的字段通过使用 const 关键字而变得不可变。
keyword.

Functional data structures have the property of being persistent: updating the data structure with one of its operations does not change the existing version of the data structure but instead produces a new version. Both exist and both can still be accessed. A good language implementation will ensure that any parts of the data structure that are not changed by an operation will be shared between the old version and the new version. Any parts that do change will be copied so that the old version may persist. The opposite of a persistent data structure is an ephemeral data structure: changes are destructive, so that only one version exists at any time. Both persistent and ephemeral data structures can be built in both functional and imperative languages.
函数式数据结构具有持久性的特性:使用其操作之一更新数据结构不会更改数据结构的现有版本,而是会生成新版本。两者都存在并且仍然可以访问。良好的语言实现将确保数据结构中未被操作更改的任何部分将在旧版本和新版本之间共享。任何发生更改的部分都将被复制,以便旧版本可以保留。持久数据结构的反面是短暂数据结构:更改具有破坏性,因此任何时候都只存在一个版本。持久数据结构和短暂数据结构都可以用函数式语言和命令式语言构建。

5.6.1. Lists 5.6.1. 列表 ¶

The built-in singly-linked list data structure in OCaml is functional. We know that, because we’ve seen how to implement it with algebraic data types. It’s also persistent, which we can demonstrate:
OCaml 中内置的单链接 list 数据结构是有效的。我们知道这一点,因为我们已经了解了如何使用代数数据类型来实现它。它也是持久的,我们可以证明这一点:

let lst = [1; 2];;
let lst' = List.tl lst;;
lst;;
val lst : int list = [1; 2]
val lst' : int list = [2]
- : int list = [1; 2]

Taking the tail of lst does not change the list. Both lst and lst' coexist without affecting one another.
获取 lst 的尾部不会更改列表。 lstlst' 共存,互不影响。

5.6.2. Stacks 5.6.2. 堆栈 ¶

We implemented stacks earlier in this chapter. Here’s a terse variant of one of those implementations, in which we add a to_list operation to make it easier to view the contents of the stack in examples:
我们在本章前面实现了堆栈。下面是其中一个实现的简洁变体,其中我们添加了 to_list 操作,以便更轻松地在示例中查看堆栈的内容:

module type Stack = sig
  type 'a t
  exception Empty
  val empty : 'a t
  val is_empty : 'a t -> bool
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a
  val pop : 'a t -> 'a t
  val size : 'a t -> int
  val to_list : 'a t -> 'a list
end

module ListStack : Stack = struct
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push = List.cons
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
  let size = List.length
  let to_list = Fun.id
end
module type Stack =
  sig
    type 'a t
    exception Empty
    val empty : 'a t
    val is_empty : 'a t -> bool
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val pop : 'a t -> 'a t
    val size : 'a t -> int
    val to_list : 'a t -> 'a list
  end
module ListStack : Stack

That implementation is functional, as can be seen above, and also persistent:
正如上面所看到的,该实现是功能性的,并且也是持久的:

open ListStack;;
let s = empty |> push 1 |> push 2;;
let s' = pop s;;
to_list s;;
to_list s';;
val s : int ListStack.t = <abstr>
val s' : int ListStack.t = <abstr>
- : int list = [2; 1]
- : int list = [1]

The value s is unchanged by the pop operation that creates s'. Both versions of the stack coexist.
创建 s'pop 操作不会更改值 s 。堆栈的两个版本共存。

The Stack module type gives us a strong hint that the data structure is persistent in the types it provides for push and pop:
Stack 模块类型给了我们一个强烈的暗示,即数据结构在它为 pushpop 提供的类型中是持久的:

val push : 'a -> 'a t -> 'a t
val pop : 'a t -> 'a t

Both of those take a stack as an argument and return a new stack as a result. An ephemeral data structure usually would not bother to return a stack. In Java, for example, similar methods might have a void return type; the equivalent in OCaml would be returning unit.
两者都以堆栈作为参数并返回一个新堆栈作为结果。临时数据结构通常不会费心返回堆栈。例如,在 Java 中,类似的方法可能具有 void 返回类型; OCaml 中的等效项将返回 unit

5.6.3. Options vs Exceptions
5.6.3. 选项与例外 ¶

All of our stack implementations so far have raised an exception whenever peek or pop is applied to the empty stack. Another possibility would be to use an option for the return value. If the input stack is empty, then peek and pop return None; otherwise, they return Some.
到目前为止,每当 peekpop 应用于空堆栈时,我们的所有堆栈实现都会引发异常。另一种可能性是使用 option 作为返回值。如果输入堆栈为空,则 peekpop 返回 None ;否则,它们返回 Some

module type Stack = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a option
  val pop : 'a t -> 'a t option
  val size : 'a t -> int
  val to_list : 'a t -> 'a list
end

module ListStack : Stack = struct
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push = List.cons
  let peek = function [] -> None | x :: _ -> Some x
  let pop = function [] -> None | _ :: s -> Some s
  let size = List.length
  let to_list = Fun.id
end
module type Stack =
  sig
    type 'a t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a option
    val pop : 'a t -> 'a t option
    val size : 'a t -> int
    val to_list : 'a t -> 'a list
  end
module ListStack : Stack

But that makes it harder to pipeline:
但这使得管道化变得更加困难:

ListStack.(empty |> push 1 |> pop |> peek)
File "[5]", line 1, characters 11-33:
1 | ListStack.(empty |> push 1 |> pop |> peek)
               ^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type int ListStack.t option
       but an expression was expected of type 'a ListStack.t

The types break down for the pipeline right after the pop, because that now returns an 'a t option, but peek expects an input that is merely an 'a t.
管道的类型在 pop 之后立即分解,因为现在返回 'a t option ,但 peek 期望的输入仅仅是 'a t

It is possible to define some additional operators to help restore the ability to pipeline. In fact, these functions are already defined in the Option module in the standard library, though not as infix operators:
可以定义一些额外的运算符来帮助恢复管道的能力。事实上,这些函数已经在标准库的 Option 模块中定义,尽管不是作为中缀运算符:

(* Option.map aka fmap *)
let ( >>| ) opt f =
  match opt with
  | None -> None
  | Some x -> Some (f x)

(* Option.bind *)
let ( >>= ) opt f =
  match opt with
  | None -> None
  | Some x -> f x
val ( >>| ) : 'a option -> ('a -> 'b) -> 'b option = <fun>
val ( >>= ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>

We can use those as needed for pipelining:
我们可以根据需要使用它们来进行管道传输:

ListStack.(empty |> push 1 |> pop >>| push 2 >>= pop >>| push 3 >>| to_list)
- : int list option = Some [3]

But it’s not so pleasant to figure out which of the three operators to use where.
但弄清楚在哪里使用这三个运算符中的哪一个并不是那么令人愉快。

但弄清楚在哪里使用这三个运算符中的哪一个并不是那么令人愉快。


因此,在界面设计上需要进行权衡:
There is therefore a tradeoff in the interface design:

  • Using options ensures that surprising exceptions regarding empty stacks never occur at run-time. The program is therefore more robust. But the convenient pipeline operator is lost.
    使用选项可确保在运行时永远不会发生有关空堆栈的令人惊讶的异常。因此该程序更加稳健。但便利的管道操作员却失去了。

  • Using exceptions means that programmers don’t have to write as much code. If they are sure that an exception can’t occur, they can omit the code for handling it. The program is less robust, but writing it is more convenient.
    使用异常意味着程序员不必编写那么多代码。如果他们确定不会发生异常,则可以省略处理异常的代码。该程序不太健壮,但编写起来更方便。

There is thus a tradeoff between writing more code early (with options) or doing more debugging later (with exceptions). The OCaml standard library has recently begun providing both versions of the interface in a data structure, so that the client can make the choice of how they want to use it. For example, we could provide both peek and peek_opt, and the same for pop, for clients of our stack module:
因此,在尽早编写更多代码(带选项)和稍后进行更多调试(有例外)之间存在权衡。 OCaml 标准库最近开始在数据结构中提供两个版本的接口,以便客户端可以选择他们想要的使用方式。例如,我们可以为堆栈模块的客户端提供 peekpeek_opt ,以及 pop

module type Stack = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a
  val peek_opt : 'a t -> 'a option
  val pop : 'a t -> 'a t
  val pop_opt : 'a t -> 'a t option
  val size : 'a t -> int
  val to_list : 'a t -> 'a list
end

module ListStack : Stack = struct
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let push = List.cons
  let peek = function [] -> raise Empty | x :: _ -> x
  let peek_opt = function [] -> None | x :: _ -> Some x
  let pop = function [] -> raise Empty | _ :: s -> s
  let pop_opt = function [] -> None | _ :: s -> Some s
  let size = List.length
  let to_list = Fun.id
end
module type Stack =
  sig
    type 'a t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val peek_opt : 'a t -> 'a option
    val pop : 'a t -> 'a t
    val pop_opt : 'a t -> 'a t option
    val size : 'a t -> int
    val to_list : 'a t -> 'a list
  end
module ListStack : Stack

One nice thing about this implementation is that it is efficient. All the operations except for size are constant time. We saw earlier in the chapter that size could be made constant time as well, at the cost of some extra space — though just a constant factor more — by caching the size of the stack at each node in the list.
此实现的一个好处是它非常高效。除 size 外的所有操作都是恒定时间。我们在本章前面看到,通过缓存列表中每个节点的堆栈大小,也可以使 size 的时间恒定,但需要一些额外的空间(尽管只是多一个常数因子) 。

5.6.4. Queues 5.6.4. 队列 ¶

Queues and stacks are fairly similar interfaces. We’ll stick with exceptions instead of options for now.
队列和堆栈是非常相似的接口。我们现在将坚持使用例外而不是选项。

module type Queue = sig
  (** An ['a t] is a queue whose elements have type ['a]. *)
  type 'a t

  (** Raised if [front] or [dequeue] is applied to the empty queue. *)
  exception Empty

  (** [empty] is the empty queue. *)
  val empty : 'a t

  (** [is_empty q] is whether [q] is empty. *)
  val is_empty : 'a t -> bool

  (** [enqueue x q] is the queue [q] with [x] added to the end. *)
  val enqueue : 'a -> 'a t -> 'a t

  (** [front q] is the element at the front of the queue. Raises [Empty]
      if [q] is empty. *)
  val front : 'a t -> 'a

  (** [dequeue q] is the queue containing all the elements of [q] except the
      front of [q]. Raises [Empty] is [q] is empty. *)
  val dequeue : 'a t -> 'a t

  (** [size q] is the number of elements in [q]. *)
  val size : 'a t -> int

  (** [to_list q] is a list containing the elements of [q] in order from
      front to back. *)
  val to_list : 'a t -> 'a list
end
module type Queue =
  sig
    type 'a t
    exception Empty
    val empty : 'a t
    val is_empty : 'a t -> bool
    val enqueue : 'a -> 'a t -> 'a t
    val front : 'a t -> 'a
    val dequeue : 'a t -> 'a t
    val size : 'a t -> int
    val to_list : 'a t -> 'a list
  end

Important 重要的

Similarly to peek and pop, note how front and dequeue divide the responsibility of getting the first element vs. getting all the rest of the elements.
peekpop 类似,请注意 frontdequeue 如何划分获取第一个元素与获取所有其余元素的责任要素。

It’s easy to implement queues with lists, just as it was for implementing stacks:
使用列表实现队列很容易,就像实现堆栈一样:

module ListQueue : Queue = struct
  (** The list [x1; x2; ...; xn] represents the queue with [x1] at its front,
      followed by [x2], ..., followed by [xn]. *)
  type 'a t = 'a list
  exception Empty
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let enqueue x q = q @ [x]
  let front = function [] -> raise Empty | x :: _ -> x
  let dequeue = function [] -> raise Empty | _ :: q -> q
  let size = List.length
  let to_list = Fun.id
end
module ListQueue : Queue

But despite being as easy, this implementation is not as efficient as our list-based stacks. Dequeueing is a constant-time operation with this representation, but enqueueing is a linear-time operation. That’s because dequeue does a single pattern match, whereas enqueue must traverse the entire list to append the new element at the end.
但是,尽管很简单,但这种实现不如我们基于列表的堆栈高效。使用这种表示法,出队是一个恒定时间操作,但入队是一个线性时间操作。这是因为 dequeue 执行单个模式匹配,而 enqueue 必须遍历整个列表才能在末尾附加新元素。

There’s a very clever way to do better on efficiency. We can use two lists to represent a single queue. This representation was invented by Robert Melville as part of his PhD dissertation at Cornell (Asymptotic Complexity of Iterative Computations, Jan 1981), which was advised by Prof. David Gries. Chris Okasaki (Purely Functional Data Structures, Cambridge University Press, 1988) calls these batched queues. Sometimes you will see this same implementation referred to as “implementing a queue with two stacks”. That’s because stacks and lists are so similar (as we’ve already seen) that you could rewrite pop as List.tl, and so forth.
有一种非常聪明的方法可以提高效率。我们可以使用两个列表来表示单个队列。这种表示法是 Robert Melville 在 David Gries 教授的指导下发明的,作为他在康奈尔大学博士论文的一部分(迭代计算的渐近复杂性,1981 年 1 月)。 Chris Okasaki(纯函数式数据结构,剑桥大学出版社,1988 年)将这些批处理队列称为批处理队列。有时您会看到同样的实现被称为“用两个堆栈实现队列”。这是因为堆栈和列表非常相似(正如我们已经看到的),您可以将 pop 重写为 List.tl ,依此类推。

The core idea has a Part A and a Part B. Part A is: we use the two lists to split the queue into two pieces, the inbox and outbox. When new elements are enqueued, we put them in the inbox. Eventually (we’ll soon come to how) elements are transferred from the inbox to the outbox. When a dequeue is requested, that element is removed from the outbox; or when the front element is requested, we check the outbox for it. For example, if the inbox currently had [3; 4; 5] and the outbox had [1; 2], then the front element would be 1, which is the head of the outbox. Dequeuing would remove that element and leave the inbox with just [2], which is the tail of the outbox. Likewise, enqueuing 6 would make the inbox become [3; 4; 5; 6].
核心思想有A部分和B部分。A部分是:我们使用两个列表将队列分成两部分,收件箱和发件箱。当新元素入队时,我们将它们放入收件箱中。最终(我们很快就会了解如何)元素从收件箱转移到发件箱。当请求出列时,该元素将从发件箱中删除;或者当请求前面的元素时,我们检查发件箱。例如,如果收件箱当前有 [3; 4; 5] ,发件箱有 [1; 2] ,那么前面的元素将为 1 ,它是发件箱的头部。出队将删除该元素,并在收件箱中留下 [2] ,这是发件箱的尾部。同样,排队 6 将使收件箱变成 [3; 4; 5; 6]

The efficiency of front and dequeue is very good so far. We just have to take the head or tail of the outbox, respectively, assuming it is non-empty. Those are constant-time operations. But the efficiency of enqueue is still bad. It’s linear time, because we have to append the new element to the end of the list. It’s too bad we have to use the append operator, which is inherently linear time. It would be much better if we could use cons, which is constant time.
到目前为止, frontdequeue 的效率非常好。我们只需分别取出发件箱的头部或尾部,假设它非空。这些是恒定时间操作。但是 enqueue 的效率还是很差。这是线性时间,因为我们必须将新元素附加到列表的末尾。遗憾的是我们必须使用追加运算符,这本质上是线性时间。如果我们可以使用 cons,即常数时间,那就更好了。

So here’s Part B of the core idea: let’s keep the inbox in reverse order. For example, if we enqueued 3 then 4 then 5, the inbox would actually be [5; 4; 3], not [3; 4; 5]. Then if 6 were enqueued next, we could cons it onto the beginning of the inbox, which becomes [6; 5; 4; 3]. The queue represented by inbox i and outbox o is therefore o @ List.rev i. So enqueue can now always be a constant-time operation.
这是核心思想的 B 部分:让收件箱保持相反的顺序。例如,如果我们将 3 入队,然后 4 然后 5 入队,则收件箱实际上是 [5; 4; 3] ,而不是 [3; 4; 5] .然后,如果 6 接下来入队,我们可以将其放在收件箱的开头,即变为 [6; 5; 4; 3] 。因此,收件箱 i 和发件箱 o 表示的队列是 o @ List.rev i 。因此 enqueue 现在可以始终是恒定时间操作。

But what about dequeue (and front)? They’re constant time too, as long as the outbox is not empty. If it’s empty, we have a problem. We need to transfer whatever is in the inbox to the outbox at that point. For example, if the outbox is empty, and the inbox is [6; 5; 4; 3], then we need to switch them around, making the outbox be [3; 4; 5; 6] and the inbox be empty. That’s actually easy: we just have to reverse the list.
但是 dequeue (和 front )呢?只要发件箱不为空,它们的时间也是恒定的。如果它是空的,我们就有问题了。此时我们需要将收件箱中的所有内容传输到发件箱。例如,如果发件箱为空,收件箱为 [6; 5; 4; 3] ,那么我们需要将它们交换过来,使发件箱为 [3; 4; 5; 6] ,收件箱为空。这实际上很简单:我们只需颠倒列表即可。

Unfortunately, we just re-introduced a linear-time operation. But with one crucial difference: we don’t have to do that linear-time reverse on every dequeue, whereas with ListQueue above we had to do the linear-time append on every enqueue. Instead, we only have to do the reverse on those rare occasions when the outbox becomes empty.
不幸的是,我们刚刚重新引入了线性时间操作。但有一个关键的区别:我们不必在每个 dequeue 上执行线性时间反转,而对于上面的 ListQueue ,我们必须在每个 enqueue 上执行线性时间附加。相反,我们只需要在发件箱变空的极少数情况下执行相反的操作。

So even though in the worst case dequeue (and front) will be linear time, most of the time they will not be. In fact, later in this book when we study amortized analysis we will show that in the long run they can be understood as constant-time operations. For now, here’s a piece of intuition to support that claim: every individual element enters the inbox once (with a cons), moves to the outbox once (with a pattern match then cons), and leaves the outbox once (with a pattern match). Each of those is constant time. So each element only ever experiences constant-time operations from its own perspective.
因此,即使在最坏的情况下 dequeue (和 front )将是线性时间,但大多数情况下它们不会。事实上,在本书后面,当我们研究摊销分析时,我们将表明,从长远来看,它们可以被理解为常数时间操作。现在,有一个直觉可以支持这一说法:每个单独的元素进入收件箱一次(有一个缺点),移动到发件箱一次(先有模式匹配,然后有缺点),然后离开发件箱一次(有一个模式匹配) )。其中每一个都是恒定时间。因此,每个元素仅从自己的角度经历恒定时间操作。

For now, let’s move on to implementing these ideas. In the implementation, we’ll add one more idea: the outbox always has to have an element in it, unless the queue is empty. In other words, if the outbox is empty, we’re guaranteed the inbox is too. That requirement isn’t necessary for batched queues, but it does keep the code simpler by reducing the number of times we have to check whether a list is empty. The tiny tradeoff is that if the queue is empty, enqueue now has to directly put an element into the outbox. No matter, that’s still a constant-time operation.
现在,让我们继续实施这些想法。在实现中,我们将添加一个想法:发件箱中始终必须有一个元素,除非队列为空。换句话说,如果发件箱是空的,我们保证收件箱也是空的。对于批处理队列来说,这个要求不是必需的,但它确实通过减少我们检查列表是否为空的次数来使代码更简单。微小的权衡是,如果队列为空, enqueue 现在必须直接将元素放入发件箱。无论如何,这仍然是一个恒定时间的操作。

module BatchedQueue : Queue = struct
  (** [{o; i}] represents the queue [o @ List.rev i]. For example,
      [{o = [1; 2]; i = [5; 4; 3]}] represents the queue [1, 2, 3, 4, 5],
      where [1] is the front element. To avoid ambiguity about emptiness,
      whenever only one of the lists is empty, it must be [i]. For example,
      [{o = [1]; i = []}] is a legal representation, but [{o = []; i = [1]}]
      is not. This implies that if [o] is empty, [i] must also be empty. *)
  type 'a t = {o : 'a list; i : 'a list}

  exception Empty

  let empty = {o = []; i = []}

  let is_empty = function
    | {o = []} -> true
    | _ -> false

  let enqueue x = function
    | {o = []} -> {o = [x]; i = []}
    | {o; i} -> {o; i = x :: i}

  let front = function
    | {o = []} -> raise Empty
    | {o = h :: _} -> h

  let dequeue = function
    | {o = []} -> raise Empty
    | {o = [_]; i} -> {o = List.rev i; i = []}
    | {o = _ :: t; i} -> {o = t; i}

  let size {o; i} = List.(length o + length i)

  let to_list {o; i} = o @ List.rev i
end
module BatchedQueue : Queue

The efficiency of batched queues comes at a price in readability. If we compare ListQueue and BatchedQueue, it’s hopefully clear that ListQueue is a simple and correct implementation of a queue data structure. It’s probably far less clear that BatchedQueue is a correct implementation. Just look at how many paragraphs of writing it took to explain it above!
批处理队列的效率是以可读性为代价的。如果我们比较 ListQueueBatchedQueue ,那么很明显 ListQueue 是队列数据结构的简单而正确的实现。 BatchedQueue 是否是正确的实现可能还不太清楚。看看上面花了多少段文字来解释!

5.6.5. Maps 5.6.5. 映射 ¶

Recall that a map (aka dictionary) binds keys to values. Here is a module type for maps. There are many other operations a map might support, but these will suffice for now.
回想一下,映射(又名字典)将键绑定到值。这是地图的模块类型。地图可能支持许多其他操作,但目前这些就足够了。

module type Map = sig
  (** [('k, 'v) t] is the type of maps that bind keys of type ['k] to
      values of type ['v]. *)
  type ('k, 'v) t

  (** [empty] does not bind any keys. *)
  val empty  : ('k, 'v) t

  (** [insert k v m] is the map that binds [k] to [v], and also contains
      all the bindings of [m].  If [k] was already bound in [m], that old
      binding is superseded by the binding to [v] in the returned map. *)
  val insert : 'k -> 'v -> ('k, 'v) t -> ('k, 'v) t

  (** [lookup k m] is the value bound to [k] in [m]. Raises: [Not_found] if [k]
      is not bound in [m]. *)
  val lookup : 'k -> ('k, 'v) t -> 'v

  (** [bindings m] is an association list containing the same bindings as [m].
      The keys in the list are guaranteed to be unique. *)
  val bindings : ('k, 'v) t -> ('k * 'v) list
end
module type Map =
  sig
    type ('k, 'v) t
    val empty : ('k, 'v) t
    val insert : 'k -> 'v -> ('k, 'v) t -> ('k, 'v) t
    val lookup : 'k -> ('k, 'v) t -> 'v
    val bindings : ('k, 'v) t -> ('k * 'v) list
  end

Note how Map.t is parameterized on two types, 'k and 'v, which are written in parentheses and separated by commas. Although ('k, 'v) might look like a pair of values, it is not: it is a syntax for writing multiple type variables.
请注意 Map.t 如何在两种类型上参数化: 'k'v ,它们写在括号中并用逗号分隔。尽管 ('k, 'v) 可能看起来像一对值,但事实并非如此:它是一种用于编写多个类型变量的语法。

Recall that association lists are lists of pairs, where the first element of each pair is a key, and the second element is the value it binds. For example, here is an association list that maps some well-known names to an approximation of their numeric value:
回想一下,关联列表是对的列表,其中每对的第一个元素是键,第二个元素是它绑定的值。例如,下面是一个关联列表,它将一些众所周知的名称映射到其数值的近似值:

[("pi", 3.14); ("e", 2.718); ("phi", 1.618)]

Naturally we can implement the Map module type with association lists:
当然,我们可以使用关联列表来实现 Map 模块类型:

module AssocListMap : Map = struct
  (** The list [(k1, v1); ...; (kn, vn)] binds key [ki] to value [vi].
      If a key appears more than once in the list, it is bound to the
      the left-most occurrence in the list. *)
  type ('k, 'v) t = ('k * 'v) list
  let empty = []
  let insert k v m = (k, v) :: m
  let lookup k m = List.assoc k m
  let keys m = List.(m |> map fst |> sort_uniq Stdlib.compare)
  let bindings m = m |> keys |> List.map (fun k -> (k, lookup k m))
end
module AssocListMap : Map

This implementation of maps is persistent. For example, adding a new binding to the map m below does not change m itself:
地图的这种实现是持久的。例如,向下面的地图 m 添加新绑定不会更改 m 本身:

open AssocListMap
let m = empty |> insert "pi" 3.14 |> insert "e" 2.718
let m' = m |> insert "phi" 1.618
let b = bindings m
let b' = bindings m'
val m : (string, float) AssocListMap.t = <abstr>
val m' : (string, float) AssocListMap.t = <abstr>
val b : (string * float) list = [("e", 2.718); ("pi", 3.14)]
val b' : (string * float) list = [("e", 2.718); ("phi", 1.618); ("pi", 3.14)]

The insert operation is constant time, which is great. But the lookup operation is linear time. It’s possible to do much better than that. In a later chapter, we’ll see how to do better. Logarithmic-time performance is achievable with balanced binary trees, and something like constant-time performance with hash tables. Neither of those, however, achieves the simplicity of the code above.
insert 操作是恒定时间,这很棒。但 lookup 操作是线性时间的。可以做得比这更好。在后面的章节中,我们将看到如何做得更好。使用平衡二叉树可以实现对数时间性能,也可以使用哈希表实现恒定时间性能。然而,这些都没有达到上面代码的简单性。

The bindings operation is complicated by potential duplicate keys in the list. It uses a keys helper function to extract the unique list of keys with the help of library function List.sort_uniq. That function sorts an input list and in the process discards duplicates. It requires a comparison function as input.
bindings 操作因列表中潜在的重复键而变得复杂。它使用 keys 辅助函数在库函数 List.sort_uniq 的帮助下提取唯一的键列表。该函数对输入列表进行排序,并在此过程中丢弃重复项。它需要一个比较函数作为输入。

Note 笔记

A comparison function must return 0 if its arguments compare as equal, a positive integer if the first is greater, and a negative integer if the first is smaller.
如果比较函数的参数比较相等,则比较函数必须返回 0;如果第一个参数较大,则必须返回正整数;如果第一个参数较小,则必须返回负整数。

Here we use the standard library’s comparison function Stdlib.compare, which behaves essentially the same as the built-in comparison operators =, <, >, etc. Custom comparison functions are useful if you want to have a relaxed notion of what being a duplicate means. For example, maybe you’d like to ignore the case of strings, or the sign of a number, etc.
这里我们使用标准库的比较函数 Stdlib.compare ,其行为与内置比较运算符 =<> 等。如果您想对重复的含义有一个轻松的概念,则自定义比较函数非常有用。例如,也许您想忽略字符串的大小写或数字的符号等。

The running time of List.sort_uniq is linearithmic, and it produces a linear number of keys as output. For each of those keys, we do a linear-time lookup operation. So the total running time of bindings is O(nlogn)+O(n)O(n), which is O(n2). We can definitely do better than that with more advanced data structures.
List.sort_uniq 的运行时间是线性的,它会产生线性数量的键作为输出。对于每个键,我们都会执行线性时间查找操作。因此 bindings 的总运行时间是 O(nlogn)+O(n)O(n) ,即 O(n2) 。我们绝对可以用更先进的数据结构做得更好。

Actually we can have a constant-time bindings operation even with association lists, if we are willing to pay for a linear-time insert operation:
实际上,如果我们愿意为线性时间 insert 操作付费,即使使用关联列表,我们也可以进行恒定时间 bindings 操作:

module UniqAssocListMap : Map = struct
  (** The list [(k1, v1); ...; (kn, vn)] binds key [ki] to value [vi].
      No duplicate keys may occur. *)
  type ('k, 'v) t = ('k * 'v) list
  let empty = []
  let insert k v m = (k, v) :: List.remove_assoc k m
  let lookup k m = List.assoc k m
  let bindings m = m
end
module UniqAssocListMap : Map

That implementation removes any duplicate binding of k before inserting a new binding.
该实现会在插入新绑定之前删除 k 的任何重复绑定。

5.6.6. Sets 5.6.6. 集 ¶

Here is a module type for sets. There are many other operations a set data structure might be expected to support, but these will suffice for now.
这是集合的模块类型。一组数据结构可能需要支持许多其他操作,但目前这些就足够了。

module type Set = sig
  (** ['a t] is the type of sets whose elements are of type ['a]. *)
  type 'a t

  (** [empty] is the empty set *)
  val empty : 'a t

  (** [mem x s] is whether [x] is an element of [s]. *)
  val mem : 'a -> 'a t -> bool

  (** [add x s] is the set that contains [x] and all the elements of [s]. *)
  val add : 'a -> 'a t -> 'a t

  (** [elements s] is a list containing the elements of [s].  No guarantee
      is made about the ordering of that list, but each is guaranteed to
      be unique. *)
  val elements : 'a t -> 'a list
end
module type Set =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
  end

Here’s an implementation of that interface using a list to represent the set. This implementation ensures that the list never contains any duplicate elements, since sets themselves do not:
下面是该接口的实现,使用列表来表示集合。此实现确保列表永远不包含任何重复元素,因为集合本身不包含:

module UniqListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add x s = if mem x s then s else x :: s
  let elements = Fun.id
end
module UniqListSet : Set

Note how add ensures that the representation never contains any duplicates, so the implementation of elements is easy. Of course, that comes with the tradeoff of add being linear time.
请注意 add 如何确保表示形式不包含任何重复项,因此 elements 的实现很容易。当然,这需要权衡 add 为线性时间。

Here’s a second implementation, which permits duplicates in the list:
这是第二个实现,它允许列表中存在重复项:

module ListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add = List.cons
  let elements s = List.sort_uniq Stdlib.compare s
end
module ListSet : Set

In that implementation, the add operation is now constant time, and the elements operation is linearithmic time.
在该实现中, add 操作现在是恒定时间, elements 操作是线性时间。

5.7. Module Type Constraints
5.7. 模块类型约束 ¶

We have extolled the virtues of encapsulation. Now we’re going to do something that might seem counter-intuitive: selectively violate encapsulation.
我们赞扬了封装的优点。现在我们要做一些看似违反直觉的事情:有选择地违反封装。

As a motivating example, here is a module type that represents values that support the usual addition and multiplication operations from arithmetic, or more precisely, a ring:
作为一个激励示例,下面是一个模块类型,它表示支持算术(或更准确地说,环)中常见的加法和乘法运算的值:

module type Ring = sig
  type t
  val zero : t
  val one : t
  val ( + ) : t -> t -> t
  val ( * ) : t -> t -> t
  val ( ~- ) : t -> t  (* additive inverse *)
  val to_string : t -> string
end
module type Ring =
  sig
    type t
    val zero : t
    val one : t
    val ( + ) : t -> t -> t
    val ( * ) : t -> t -> t
    val ( ~- ) : t -> t
    val to_string : t -> string
  end

Recall that we must write ( * ) instead of (*) because the latter would be parsed as beginning a comment. And we write the ~ in ( ~- ) to indicate a unary operator.
回想一下,我们必须编写 ( * ) 而不是 (*) ,因为后者会被解析为注释的开始。我们在 ( ~- ) 中写入 ~ 来表示一元运算符。

This is a bit weird of an example. We don’t normally think of numbers as a data structure. But what is a data structure except for a set of values and operations on them? The Ring module type makes it clear that’s what we have.
这是一个有点奇怪的例子。我们通常不认为数字是一种数据结构。但是,除了一组值和对它们的操作之外,数据结构是什么? Ring 模块类型清楚地表明这就是我们所拥有的。

Here is a module that implements that module type:
这是实现该模块类型的模块:

module IntRing : Ring = struct
  type t = int
  let zero = 0
  let one = 1
  let ( + ) = Stdlib.( + )
  let ( * ) = Stdlib.( * )
  let ( ~- ) = Stdlib.( ~- )
  let to_string = string_of_int
end
module IntRing : Ring

Because t is abstract, the toplevel can’t give us good output about what the sum of one and one is:
因为 t 是抽象的,所以顶层无法为我们提供有关 1 和 1 之和的良好输出:

IntRing.(one + one)
- : IntRing.t = <abstr>

But we could convert it to a string:
但我们可以将其转换为字符串:

IntRing.(one + one |> to_string)
- : string = "2"

We could even install a pretty printer to avoid having to manually call to_string:
我们甚至可以安装一个漂亮的打印机以避免手动调用 to_string

let pp_intring fmt i =
  Format.fprintf fmt "%s" (IntRing.to_string i);;

#install_printer pp_intring;;

IntRing.(one + one)
val pp_intring : Format.formatter -> IntRing.t -> unit = <fun>
- : IntRing.t = 2

We could implement other kinds of rings, too:
我们也可以实现其他类型的环:

module FloatRing : Ring = struct
  type t = float
  let zero = 0.
  let one = 1.
  let ( + ) = Stdlib.( +. )
  let ( * ) = Stdlib.( *. )
  let ( ~- ) = Stdlib.( ~-. )
  let to_string = string_of_float
end
module FloatRing : Ring

Then we’d have to install a printer for it, too:
然后我们还必须为其安装打印机:

let pp_floatring fmt f =
  Format.fprintf fmt "%s" (FloatRing.to_string f);;

#install_printer pp_floatring;;

FloatRing.(one + one)
val pp_floatring : Format.formatter -> FloatRing.t -> unit = <fun>
- : FloatRing.t = 2.

Was there really a need to make type t abstract in the ring examples above? Arguably not. And if it were not abstract, we wouldn’t have to go to the trouble of converting abstract values into strings, or installing printers. Let’s pursue that idea, next.
真的需要在上面的环示例中使类型 t 抽象吗?可以说不是。如果它不是抽象的,我们就不必费力将抽象值转换为字符串,或安装打印机。接下来让我们继续这个想法。

5.7.1. Specializing Module Types
5.7.1. 专门化模块类型 ¶

In the past, we’ve seen that we can leave off the module type annotation, then do a separate check to make sure the structure satisfies the signature:
在过去,我们已经看到我们可以省略模块类型注释,然后进行单独的检查以确保结构满足签名:

module IntRing = struct
  type t = int
  let zero = 0
  let one = 1
  let ( + ) = Stdlib.( + )
  let ( * ) = Stdlib.( * )
  let ( ~- ) = Stdlib.( ~- )
  let to_string = string_of_int
end

module _ : Ring = IntRing
module IntRing :
  sig
    type t = int
    val zero : int
    val one : int
    val ( + ) : int -> int -> int
    val ( * ) : int -> int -> int
    val ( ~- ) : int -> int
    val to_string : int -> string
  end
IntRing.(one + one)
- : int = 2

There’s a more sophisticated way of accomplishing the same goal. We can specialize the Ring module type to specify that t must be int or float. We do that by adding a constraint using the with keyword:
有一种更复杂的方法可以实现相同的目标。我们可以专门化 Ring 模块类型来指定 t 必须是 intfloat 。我们通过使用 with 关键字添加约束来做到这一点:

module type INT_RING = Ring with type t = int
module type INT_RING =
  sig
    type t = int
    val zero : t
    val one : t
    val ( + ) : t -> t -> t
    val ( * ) : t -> t -> t
    val ( ~- ) : t -> t
    val to_string : t -> string
  end

Note how the INT_RING module type now specifies that t and int are the same type. It exposes or shares that fact with the world, so we could call these “sharing constraints.”
请注意 INT_RING 模块类型现在如何指定 tint 是同一类型。它向世界揭露或分享这一事实,因此我们可以将这些称为“共享约束”。

Now IntRing can be given that module type:
现在 IntRing 可以被赋予该模块类型:

module IntRing : INT_RING = struct
  type t = int
  let zero = 0
  let one = 1
  let ( + ) = Stdlib.( + )
  let ( * ) = Stdlib.( * )
  let ( ~- ) = Stdlib.( ~- )
  let to_string = string_of_int
end
module IntRing : INT_RING

And since the equality of t and int is exposed, the toplevel can print values of type t without any help needed from a pretty printer:
由于 tint 的相等性已公开,因此顶层可以打印 t 类型的值,而无需漂亮打印机的任何帮助:

IntRing.(one + one)
- : IntRing.t = 2

Programmers can even mix and match built-in int values with those provided by IntRing:
程序员甚至可以将内置 int 值与 IntRing 提供的值混合和匹配:

IntRing.(1 + one)
- : IntRing.t = 2

The same can be done for floats:
对于浮动也可以做同样的事情:

module type FLOAT_RING = Ring with type t = float

module FloatRing : FLOAT_RING = struct
  type t = float
  let zero = 0.
  let one = 1.
  let ( + ) = Stdlib.( +. )
  let ( * ) = Stdlib.( *. )
  let ( ~- ) = Stdlib.( ~-. )
  let to_string = string_of_float
end
module type FLOAT_RING =
  sig
    type t = float
    val zero : t
    val one : t
    val ( + ) : t -> t -> t
    val ( * ) : t -> t -> t
    val ( ~- ) : t -> t
    val to_string : t -> string
  end
module FloatRing : FLOAT_RING

It turns out there’s no need to separately define INT_RING and FLOAT_RING. The with keyword can be used as part of the module definition, though the syntax becomes a little harder to read because of the proximity of the two = signs:
事实证明,不需要单独定义 INT_RINGFLOAT_RINGwith 关键字可以用作 module 定义的一部分,但由于两个 = 符号很接近,语法变得有点难以阅读:

module FloatRing : Ring with type t = float = struct
  type t = float
  let zero = 0.
  let one = 1.
  let ( + ) = Stdlib.( +. )
  let ( * ) = Stdlib.( *. )
  let ( ~- ) = Stdlib.( ~-. )
  let to_string = string_of_float
end
module FloatRing :
  sig
    type t = float
    val zero : t
    val one : t
    val ( + ) : t -> t -> t
    val ( * ) : t -> t -> t
    val ( ~- ) : t -> t
    val to_string : t -> string
  end

5.7.2. Constraints 5.7.2. 约束 ¶

Syntax.

There are two sorts of constraints. One is the sort we saw above, with type equations:
有两种限制。一种是我们上面看到的类型,带有 type 方程:

  • T with type x = t, where T is a module type, x is a type name, and t is a type.
    T with type x = t ,其中 T 是模块类型, x 是类型名称, t 是类型。

The other sort is a module equation, which is syntactic sugar for specifying the equality of all types in the two modules:
另一种是 module 方程,它是用于指定两个模块中所有类型相等的语法糖:

  • T with module M = N, where M and N are module names.
    T with module M = N ,其中 MN 是模块名称。

Multiple constraints can be added with the and keyword:
可以使用 and 关键字添加多个约束:

  • T with constraint1 and constraint2 and ... constraintN

Static semantics. 静态语义。

The constrained module type T with type x = t is the same as T, except that the declaration of type x inside T is replaced by type x = t. For example, compare the two signatures output below:
受约束模块类型 T with type x = tT 相同,只不过 T 中的 type x 声明被替换为 type x = t

module type T = sig type t end
module type U = T with type t = int
module type T = sig type t end
module type U = sig type t = int end

Likewise, T with module M = N is the same as T, except that the any declaration type x inside the module type of M is replaced by type x = N.x. (And the same recursively for any nested modules.) It takes more work to give and understand this example:
同样, T with module M = NT 相同,只不过 M 模块类型内的任何声明 type x 被替换为 type x = N.x 。 (对于任何嵌套模块都是递归的。)给出和理解这个例子需要更多的工作:

module type XY = sig
  type x
  type y
end

module type T = sig
  module A : XY
end

module B = struct
  type x = int
  type y = float
end

module type U = T with module A = B

module C : U = struct
  module A = struct
    type x = int
    type y = float
    let x = 42
  end
end
module type XY = sig type x type y end
module type T = sig module A : XY end
module B : sig type x = int type y = float end
module type U = sig module A : sig type x = int type y = float end end
module C : U

Focus on the output for module type U. Notice that the types of x and y in it have become int and float because of the module A = B constraint. Also notice how modules B and C.A are not the same module; the latter has an extra item x in it. So the syntax module A = B is potentially confusing. The constraint is not specifying that the two modules are the same. Rather, it specifies that all their types are constrained to be equal.
重点关注模块类型 U 的输出。请注意,由于 module A = B 约束,其中 xy 的类型已变为 intfloat 。另请注意模块 BC.A 不是同一个模块;后者有一个额外的项目 x 。因此语法 module A = B 可能会令人困惑。该约束并未指定两个模块相同。相反,它指定它们的所有类型都必须相等。

Dynamic semantics. 动态语义。

There are no dynamic semantics for constraints, because they are only for type checking.
约束没有动态语义,因为它们仅用于类型检查。

5.8. Includes 5.8. 包括 ¶

Copying and pasting code is almost always a bad idea. Duplication of code causes duplication and proliferation of errors. So why are we so prone to making this mistake? Maybe because it always seems like the easier option — easier and quicker than applying the Abstraction Principle as we should to factor out common code.
复制和粘贴代码几乎总是一个坏主意。代码重复会导致错误的重复和扩散。那么为什么我们这么容易犯这个错误呢?也许是因为它似乎总是更容易的选择——比应用抽象原则更容易、更快,因为我们应该分解出公共代码。

The OCaml module system provides a neat feature called includes that is like a principled copy-and-paste that is quick and easy to use, but avoids actual duplication. It can be used to solve some of the same problems as inheritance in object-oriented languages.
OCaml 模块系统提供了一个称为“包含”的简洁功能,它就像原则性的复制粘贴一样,快速且易于使用,但避免了实际的重复。它可以用来解决一些与面向对象语言中的继承相同的问题。

Let’s start with an example. Recall this implementation of sets as lists:
让我们从一个例子开始。回想一下集合作为列表的实现:

module type Set = sig
  type 'a t
  val empty : 'a t
  val mem : 'a -> 'a t -> bool
  val add : 'a -> 'a t -> 'a t
  val elements : 'a t -> 'a list
end

module ListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add = List.cons
  let elements s = List.sort_uniq Stdlib.compare s
end
module type Set =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
  end
module ListSet : Set

Suppose we wanted to add a function of_list : 'a list -> 'a t that could construct a set out of a list. If we had access to the source code of both ListSet and Set, and if we were permitted to modify it, this wouldn’t be hard. But what if they were third-party libraries for which we didn’t have source code?
假设我们想要添加一个函数 of_list : 'a list -> 'a t ,它可以从列表中构造一个集合。如果我们能够访问 ListSetSet 的源代码,并且允许修改它,那么这并不难。但是,如果它们是我们没有源代码的第三方库怎么办?

In Java, we might use inheritance to solve this problem:
在Java中,我们可以使用继承来解决这个问题:

interface Set<T> { ... }
class ListSet<T> implements Set<T> { ... }
class ListSetExtended<T> extends ListSet<T> {
  Set<T> ofList(List<T> lst) { ... }
}

That helps us to reuse code, because the subclass inherits all the methods of its superclass.
这有助于我们重用代码,因为子类继承了其超类的所有方法。

OCaml includes are similar. They enable a module to include all the items defined by another module, or a module type to include all the specifications of another module type.
OCaml 包含类似的内容。它们使一个模块能够包含另一个模块定义的所有项目,或者一个模块类型包含另一个模块类型的所有规范。

Here’s how we can use includes to solve the problem of adding of_list to ListSet:
下面是我们如何使用 include 来解决将 of_list 添加到 ListSet 的问题:

module ListSetExtended = struct
  include ListSet
  let of_list lst = List.fold_right add lst empty
end
module ListSetExtended :
  sig
    type 'a t = 'a ListSet.t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a t
  end

This code says that ListSetExtended is a module that includes all the definitions of the ListSet module, as well as a definition of of_list. We don’t have to know the source code implementing ListSet to make this happen.
此代码表示 ListSetExtended 是一个模块,其中包含 ListSet 模块的所有定义以及 of_list 的定义。我们不必知道实现 ListSet 的源代码即可实现这一点。

Note 笔记

You might wonder why we can’t simply implement of_list as the identity function. See the section below on encapsulation for the answer.
您可能想知道为什么我们不能简单地实现 of_list 作为恒等函数。请参阅下面有关封装的部分以获得答案。

5.8.1. Semantics of Includes
5.8.1. 包括的语义 ¶

Includes can be used inside of structures and signatures. When we include inside a signature, we must be including another signature. And when we include inside a structure, we must be including another structure.
包括可以在结构和签名内部使用。当我们在签名中包括时,我们必须包含另一个签名。当我们包括一个结构内部时,我们必须包括另一个结构。

Including a structure is effectively just syntactic sugar for writing a local definition for each name defined in the module. Writing include ListSet as we did above, for example, has an effect similar to writing the following:
包含结构实际上只是为模块中定义的每个名称编写本地定义的语法糖。例如,像上面那样编写 include ListSet 具有类似于编写以下内容的效果:

module ListSetExtended = struct
  (* BEGIN all the includes *)
  type 'a t = 'a ListSet.t
  let empty = ListSet.empty
  let mem = ListSet.mem
  let add = ListSet.add
  let elements = ListSet.elements
  (* END all the includes *)
  let of_list lst = List.fold_right add lst empty
end
module ListSetExtended :
  sig
    type 'a t = 'a ListSet.t
    val empty : 'a ListSet.t
    val mem : 'a -> 'a ListSet.t -> bool
    val add : 'a -> 'a ListSet.t -> 'a ListSet.t
    val elements : 'a ListSet.t -> 'a list
    val of_list : 'a list -> 'a ListSet.t
  end

None of that is actually copying the source code of ListSet. Rather, the include just creates a new definition in ListSetExtended with the same name as each definition in ListSet. But if the set of names defined inside ListSet ever changed, the include would reflect that change, whereas a copy-paste job would not.
这些实际上都没有复制 ListSet 的源代码。相反, include 只是在 ListSetExtended 中创建一个新定义,其名称与 ListSet 中的每个定义相同。但是,如果 ListSet 中定义的名称集发生更改, include 将反映该更改,而复制粘贴作业则不会。

Including a signature is much the same. For example, we could write:
包括签名大致相同。例如,我们可以写:

module type SetExtended = sig
  include Set
  val of_list : 'a list -> 'a t
end
module type SetExtended =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a t
  end

Which would have an effect similar to writing the following:
这将产生类似于编写以下内容的效果:

module type SetExtended = sig
  (* BEGIN all the includes *)
  type 'a t
  val empty : 'a t
  val mem : 'a -> 'a t -> bool
  val add : 'a -> 'a t -> 'a t
  val elements  : 'a t -> 'a list
  (* END all the includes *)
  val of_list : 'a list -> 'a t
end
module type SetExtended =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a t
  end

That module type would be suitable for ListSetExtended:
该模块类型适合 ListSetExtended

module ListSetExtended : SetExtended = struct
  include ListSet
  let of_list lst = List.fold_right add lst empty
end
module ListSetExtended : SetExtended

5.8.2. Encapsulation and Includes
5.8.2. 封装和包含 ¶

We mentioned above that you might wonder why we didn’t write this simpler definition of of_list:
我们上面提到过,您可能想知道为什么我们不编写这个更简单的 of_list 定义:

module ListSetExtended : SetExtended = struct
  include ListSet
  let of_list lst = lst
end
File "[7]", lines 1-4, characters 39-3:
1 | .......................................struct
2 |   include ListSet
3 |   let of_list lst = lst
4 | end
Error: Signature mismatch:
       ...
       Values do not match:
         val of_list : 'a -> 'a
       is not included in
         val of_list : 'a list -> 'a t
       The type 'a list -> 'a list is not compatible with the type
         'a list -> 'a t
       Type 'a list is not compatible with type 'a t = 'a ListSet.t 
       File "[5]", line 9, characters 2-31: Expected declaration
       File "[7]", line 3, characters 6-13: Actual declaration

Check out that error message. It looks like of_list doesn’t have the right type. What if we try adding some type annotations?
查看该错误消息。看起来 of_list 的类型不正确。如果我们尝试添加一些类型注释怎么办?

module ListSetExtended : SetExtended = struct
  include ListSet
  let of_list (lst : 'a list) : 'a t = lst
end
File "[8]", line 3, characters 39-42:
3 |   let of_list (lst : 'a list) : 'a t = lst
                                           ^^^
Error: This expression has type 'a list
       but an expression was expected of type 'a t = 'a ListSet.t

Ah, now the problem is clearer: in the body of of_list, the equality of 'a t and 'a list isn’t known. In ListSetExtended, we do know that 'a t = 'a ListSet.t, because that’s what the include gave us. But the fact that 'a ListSet.t = 'a list was hidden when ListSet was sealed at module type Set. So, includes must obey encapsulation, just like the rest of the module system.
啊,现在问题更清楚了:在 of_list 的主体中, 'a t'a list 的相等性是未知的。在 ListSetExtended 中,我们确实知道 'a t = 'a ListSet.t ,因为这是 include 给我们的。但事实上,当 ListSet 被密封在模块类型 Set 时, 'a ListSet.t = 'a list 被隐藏了。因此,包含必须遵守封装,就像模块系统的其余部分一样。

One workaround is to rewrite the definitions as follows:
一种解决方法是重写定义,如下所示:

module ListSetImpl = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add = List.cons
  let elements s = List.sort_uniq Stdlib.compare s
end

module ListSet : Set = ListSetImpl

module type SetExtended = sig
  include Set
  val of_list : 'a list -> 'a t
end

module ListSetExtendedImpl = struct
  include ListSetImpl
  let of_list lst = lst
end

module ListSetExtended : SetExtended = ListSetExtendedImpl
module ListSetImpl :
  sig
    type 'a t = 'a list
    val empty : 'a list
    val mem : 'a -> 'a list -> bool
    val add : 'a -> 'a list -> 'a list
    val elements : 'a list -> 'a list
  end
module ListSet : Set
module type SetExtended =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a t
  end
module ListSetExtendedImpl :
  sig
    type 'a t = 'a list
    val empty : 'a list
    val mem : 'a -> 'a list -> bool
    val add : 'a -> 'a list -> 'a list
    val elements : 'a list -> 'a list
    val of_list : 'a -> 'a
  end
module ListSetExtended : SetExtended

The important change is that ListSetImpl is not sealed, so its type 'a t is not abstract. When we include it in ListSetExtended, we can therefore exploit the fact that it’s a synonym for 'a list.
重要的变化是 ListSetImpl 不是密封的,因此它的类型 'a t 不是抽象的。当我们将它包含在 ListSetExtended 中时,我们可以利用它是 'a list 的同义词这一事实。

What we just did is effectively the same as what Java does to handle the visibility modifiers public, private, etc. The “private version” of a class is like the Impl version above: anyone who can see that version gets to see all the exposed items (fields in Java, types in OCaml), without any encapsulation. The “public version” of a class is like the sealed version above: anyone who can see that version is forced to treat the items as abstract, hence encapsulated.
我们刚才所做的实际上与 Java 处理可见性修饰符 publicprivate 等的操作相同。类的“私有版本”就像 Impl

With that technique, if we want to provide a new implementation of one of the included functions we could do that too:
通过这种技术,如果我们想提供其中一个函数的新实现,我们也可以这样做:

module ListSetExtendedImpl = struct
  include ListSetImpl
  let of_list lst = List.fold_right add lst empty
  let rec elements = function
    | [] -> []
    | h :: t -> if mem h t then elements t else h :: elements t
end
module ListSetExtendedImpl :
  sig
    type 'a t = 'a list
    val empty : 'a list
    val mem : 'a -> 'a list -> bool
    val add : 'a -> 'a list -> 'a list
    val of_list : 'a list -> 'a list
    val elements : 'a list -> 'a list
  end

But that’s a bad idea. First, it’s actually a quadratic implementation of elements instead of linearithmic. Second, it does not replace the original implementation of elements. Remember the semantics of modules: all definitions are evaluated from top to bottom, in order. So the new definition of elements above won’t come into use until the very end of evaluation. If any earlier functions had happened to use elements as a helper, they would use the original linearithmic version, not the new quadratic version.
但这是个坏主意。首先,它实际上是 elements 的二次实现,而不是线性实现。其次,它不会取代 elements 的原始实现。请记住模块的语义:所有定义均按从上到下的顺序进行评估。因此,上面 elements 的新定义要到评估结束时才会使用。如果任何早期的函数碰巧使用 elements 作为辅助函数,它们将使用原始的线性版本,而不是新的二次版本。

Warning 警告

This differs from what you might expect from Java, which uses a language feature called dynamic dispatch to figure out which method implementation to invoke. Dynamic dispatch is arguably the defining feature of object-oriented languages. OCaml functions are not methods, and they do not use dynamic dispatch.
这与您对 Java 的期望不同,Java 使用称为动态调度的语言功能来确定要调用哪个方法实现。动态分派可以说是面向对象语言的定义特征。 OCaml 函数不是方法,并且它们不使用动态调度。

5.8.3. Include vs. Open
5.8.3. 包括 vs 打开 ¶

The include and open statements are quite similar, but they have a subtly different effect on a structure. Consider this code:
includeopen 语句非常相似,但它们对结构的影响略有不同。考虑这段代码:

module M = struct
  let x = 0
end

module N = struct
  include M
  let y = x + 1
end

module O = struct
  open M
  let y = x + 1
end
module M : sig val x : int end
module N : sig val x : int val y : int end
module O : sig val y : int end

Look closely at the values contained in each structure. N has both an x and y, whereas O has only a y. The reason is that include M causes all the definitions of M to also be included in N, so the definition of x from M is present in N. But open M only made those definitions available in the scope of O; it doesn’t actually make them part of the structure. So O does not contain a definition of x, even though x is in scope during the evaluation of O’s definition of y.
仔细查看每个结构中包含的值。 N 同时具有 xy ,而 O 仅具有 y 。原因是 include M 导致 M 的所有定义也包含在 N 中,因此 M 存在于 N 中。但是 open M 仅使这些定义在 O 范围内可用;它实际上并没有使它们成为结构的一部分。因此, O 不包含 x 的定义,即使 x 在评估 O 定义中的 y 的期间位于范围内。

A metaphor for understanding this difference might be: open M imports definitions from M and makes them available for local consumption, but they aren’t exported to the outside world. Whereas include M imports definitions from M, makes them available for local consumption, and additionally exports them to the outside world.
理解这种差异的一个比喻可能是: open MM 导入定义并使它们可供本地使用,但它们不会导出到外部世界。而 include MM 导入定义,使它们可供本地使用,并另外将它们导出到外部世界。

5.8.4. Including Code in Multiple Modules
5.8.4. 在多个模块中包含代码 ¶

Recall that we also had an implementation of sets that made sure every element of the underlying list was unique:
回想一下,我们还有一个集合的实现,可以确保底层列表的每个元素都是唯一的:

module UniqListSet : Set = struct
  (** All values in the list must be unique. *)
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add x s = if mem x s then s else x :: s
  let elements = Fun.id
end
module UniqListSet : Set

Suppose we wanted to add of_list to that module too. One possibility would be to copy and paste that function from ListSet into UniqListSet. But that’s poor software engineering. So let’s rule that out right away as a non-solution.
假设我们也想将 of_list 添加到该模块。一种可能性是将该函数从 ListSet 复制并粘贴到 UniqListSet 中。但这是糟糕的软件工程。因此,让我们立即将其排除为非解决方案。

Instead, suppose we try to define the function outside of either module:
相反,假设我们尝试在任一模块之外定义该函数:

let of_list lst = List.fold_right add lst empty
File "[13]", line 1, characters 34-37:
1 | let of_list lst = List.fold_right add lst empty
                                      ^^^
Error: Unbound value add

The problem is we either need to choose which module’s add and empty we want. But as soon as we do, the function becomes useful only with that one module:
问题是我们要么需要选择我们想要的模块的 addempty 。但一旦我们这样做了,该函数就只对那个模块有用:

let of_list lst = List.fold_right ListSet.add lst ListSet.empty
val of_list : 'a list -> 'a ListSet.t = <fun>

We could make add and empty be parameters instead:
我们可以将 addempty 作为参数:

let of_list' add empty lst = List.fold_right add lst empty

let of_list lst = of_list' ListSet.add ListSet.empty lst
let of_list_uniq lst = of_list' UniqListSet.add UniqListSet.empty lst
val of_list' : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b = <fun>
val of_list : 'a list -> 'a ListSet.t = <fun>
val of_list_uniq : 'a list -> 'a UniqListSet.t = <fun>

But this is annoying in a couple of ways. First, we have to remember which function name to call, whereas all the other operations that are part of those modules have the same name, regardless of which module they’re in. Second, the of_list functions live outside either module, so clients who open one of the modules won’t automatically get the ability to name those functions.
但这在几个方面都很烦人。首先,我们必须记住要调用哪个函数名称,而属于这些模块的所有其他操作都具有相同的名称,无论它们位于哪个模块中。其次, of_list 函数位于外部任一模块,因此打开其中一个模块的客户端不会自动获得命名这些函数的能力。

Let’s try to use includes to solve this problem. First, we write a module that contains the parameterized implementation:
我们尝试使用include来解决这个问题。首先,我们编写一个包含参数化实现的模块:

module SetOfList = struct
  let of_list' add empty lst = List.fold_right add lst empty
end
module SetOfList :
  sig val of_list' : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b end

Then we include that module to get the helper function:
然后我们包含该模块来获取辅助函数:

module UniqListSetExtended : SetExtended = struct
  include UniqListSet
  include SetOfList
  let of_list lst = of_list' add empty lst
end

module ListSetExtended : SetExtended = struct
  include ListSet
  include SetOfList
  let of_list lst = of_list' add empty lst
end
module UniqListSetExtended : SetExtended
module ListSetExtended : SetExtended

That works, but we’ve only partially succeeded in achieving code reuse:
这是可行的,但我们只部分成功地实现了代码重用:

  • On the positive side, the code that implements of_list' has been factored out into a single location and reused in the two structures.
    从积极的一面来看,实现 of_list' 的代码已被分解到一个位置并在两个结构中重用。

  • But on the negative side, we still had to write an implementation of of_list in both modules. Worse yet, those implementations are identical. So there’s still code duplication occurring.
    但不利的一面是,我们仍然必须在两个模块中编写 of_list 的实现。更糟糕的是,这些实现是相同的。所以仍然存在代码重复的情况。

Could we do better? Yes. And that leads us to functors, next.
我们可以做得更好吗?是的。接下来我们就到了函子。

5.9. Functors 5.9. 函子 ¶

The problem we were having in the previous section was that we wanted to add code to two different modules, but that code needed to be parameterized on the details of the module to which it was being added. It’s that kind of parameterization that is enabled by an OCaml language feature called functors.
我们在上一节中遇到的问题是,我们想要将代码添加到两个不同的模块,但需要根据要添加的模块的详细信息对该代码进行参数化。这种参数化是由称为函子的 OCaml 语言功能启用的。

Note 笔记

Why the name “functor”? In category theory, a category contains morphisms, which are a generalization of functions as we know them, and a functor is map between categories. Likewise, OCaml modules contain functions, and OCaml functors map from modules to modules.
为什么叫“函子”?在范畴论中,范畴包含态射,态射是我们所知的函数的泛化,函子是范畴之间的映射。同样,OCaml 模块包含函数,并且 OCaml 函子从模块映射到模块。

The name is unfortunately intimidating, but a functor is simply a “function” from modules to modules. The word “function” is in quotation marks in that sentence only because it’s a kind of function that’s not interchangeable with the rest of the functions we’ve already seen. OCaml’s type system is stratified: module values are distinct from other values, so functions from modules to modules cannot be written or used in the same way as functions from values to values. But conceptually, functors really are just functions.
不幸的是,这个名字有点吓人,但函子只是一个从模块到模块的“函数”。 “函数”这个词在这句话中用引号引起来,只是因为它是一种不能与我们已经见过的其他函数互换的函数。 OCaml 的类型系统是分层的:模块值与其他值不同,因此从模块到模块的函数不能以与从值到值的函数相同的方式编写或使用。但从概念上讲,函子实际上只是函数。

Here’s a tiny example of a functor:
这是函子的一个小例子:

module type X = sig
  val x : int
end

module IncX (M : X) = struct
  let x = M.x + 1
end
module type X = sig val x : int end
module IncX : functor (M : X) -> sig val x : int end

The functor’s name is IncX. It’s essentially a function from modules to modules. As a function, it takes an input and produces an output. Its input is named M, and the type of its input is X. Its output is the structure that appears on the right-hand side of the equals sign: struct let x = M.x + 1 end.
函子的名称是 IncX 。它本质上是一个从模块到模块的函数。作为一个函数,它接受输入并产生输出。它的输入名为 M ,其输入的类型为 X 。它的输出是出现在等号右侧的结构: struct let x = M.x + 1 end

Another way to think about IncX is that it’s a parameterized structure. The parameter that it takes is named M and has type X. The structure itself has a single value named x in it. The value that x has will depend on the parameter M.
另一种思考 IncX 的方式是它是一个参数化结构。它采用的参数名为 M 且类型为 X 。该结构本身有一个名为 x 的值。 x 的值取决于参数 M

Since functors are essentially functions, we apply them. Here’s an example of applying IncX:
由于函子本质上是函数,因此我们应用它们。这是应用 IncX 的示例:

module A = struct let x = 0 end
module A : sig val x : int end
A.x
- : int = 0
module B = IncX (A)
module B : sig val x : int end
B.x
- : int = 1
module C = IncX (B)
module C : sig val x : int end
C.x
- : int = 2

Each time, we pass IncX a module. When we pass it the module bound to the name A, the input to IncX is struct let x = 0 end. Functor IncX takes that input and produces an output struct let x = A.x + 1 end. Since A.x is 0, the result is struct let x = 1 end. So B is bound to struct let x = 1 end. Similarly, C ends up being bound to struct let x = 2 end.
每次,我们都会传递 IncX 一个模块。当我们将绑定到名称 A 的模块传递给它时, IncX 的输入是 struct let x = 0 end 。函子 IncX 接受该输入并生成输出 struct let x = A.x + 1 end 。由于 A.x0 ,因此结果是 struct let x = 1 end 。因此 B 绑定到 struct let x = 1 end 。类似地, C 最终被绑定到 struct let x = 2 end

Although the functor IncX returns a module that is quite similar to its input module, that need not be the case. In fact, a functor can return any module it likes, perhaps something very different than its input structure:
尽管函子 IncX 返回一个与其输入模块非常相似的模块,但情况不一定如此。事实上,函子可以返回它喜欢的任何模块,也许是与其输入结构非常不同的东西:

module AddX (M : X) = struct
  let add y = M.x + y
end
module AddX : functor (M : X) -> sig val add : int -> int end

Let’s apply that functor to a module. The module doesn’t even have to be bound to a name; we can just write an anonymous structure:
让我们将该函子应用于模块。该模块甚至不必绑定到名称;我们可以写一个匿名结构:

module Add42 = AddX (struct let x = 42 end)
module Add42 : sig val add : int -> int end
Add42.add 1
- : int = 43

Note that the input module to AddX contains a value named x, but the output module from AddX does not:
请注意, AddX 的输入模块包含名为 x 的值,但 AddX 的输出模块不包含:

Add42.x
File "[11]", line 1, characters 0-7:
1 | Add42.x
    ^^^^^^^
Error: Unbound value Add42.x

Warning 警告

It’s tempting to think that a functor is the same as extends in Java, and that the functor therefore extends the input module with new definitions while keeping the old definitions around too. The example above shows that is not the case. A functor is essentially just a function, and that function can return whatever the programmer wants. In fact the output of the functor could be arbitrarily different than the input.
人们很容易认为仿函数与 Java 中的 extends 相同,因此仿函数用新定义扩展了输入模块,同时也保留了旧定义。上面的例子表明情况并非如此。函子本质上只是一个函数,该函数可以返回程序员想要的任何内容。事实上,函子的输出可以与输入任意不同。

5.9.1. Functor Syntax and Semantics
5.9.1. 函子语法和语义 ¶

In the functor syntax we’ve been using:
在我们一直使用的函子语法中:

module F (M : S) = ...
end

the type annotation : S and the parentheses around it, (M : S) are required. The reason why is that OCaml needs the type information about S to be provided in order to do a good job with type inference for F itself.
类型注释 : S 及其周围的括号 (M : S) 是必需的。原因是OCaml需要提供 S 的类型信息,才能做好 F 本身的类型推断。

Much like functions, functors can be written anonymously. The following two syntaxes for functors are equivalent:
与函数非常相似,函子可以匿名编写。以下两种函子语法是等效的:

module F (M : S) = ...

module F = functor (M : S) -> ...

The second form uses the functor keyword to create an anonymous functor, like how the fun keyword creates an anonymous function.
第二种形式使用 functor 关键字创建匿名函子,就像 fun 关键字创建匿名函数的方式一样。

And functors can be parameterized on multiple structures:
函子可以在多个结构上参数化:

module F (M1 : S1) ... (Mn : Sn) = ...

Of course, that’s just syntactic sugar for a higher-order functor that takes a structure as input and returns an anonymous functor:
当然,这只是高阶函子的语法糖,它以结构作为输入并返回匿名函子:

module F = functor (M1 : S1) -> ... -> functor (Mn : Sn) -> ...

If you want to specify the output type of a functor, the syntax is again similar to functions:
如果要指定函子的输出类型,语法又类似于函数:

module F (M : Si) : So = ...

As usual, it’s also possible to write the output type annotation on the module expression:
像往常一样,也可以在模块表达式上编写输出类型注释:

module F (M : Si) = (... : So)

To evaluate an application module_expression1 (module_expression2), the first module expression is evaluated and must produce a functor F. The second module expression is then evaluated to a module M. The functor is then applied to the module. The functor will be producing a new module N as part of that application. That new module is evaluated as always, in order of definition from top to bottom, with the definitions of M available for use.
要评估应用程序 module_expression1 (module_expression2) ,将评估第一个模块表达式,并且必须生成函子 F 。然后,第二个模块表达式被评估为模块 M 。然后将该函子应用于该模块。函子将生成一个新模块 N 作为该应用程序的一部分。该新模块将一如既往地按照从上到下的定义顺序进行评估,并且 M 的定义可供使用。

5.9.2. Functor Type Syntax and Semantics
5.9.2. 函子类型语法和语义 ¶

The simplest syntax for functor types is actually the same as for functions:
函子类型最简单的语法实际上与函数相同:

module_type -> module_type

For example, X -> Add below is a functor type, and it works for the AddX module we defined earlier in this section:
例如,下面的 X -> Add 是一个函子类型,它适用于我们在本节前面定义的 AddX 模块:

module type Add = sig val add : int -> int end
module CheckAddX : X -> Add = AddX
module type Add = sig val add : int -> int end
module CheckAddX : X -> Add

Functor type syntax becomes more complicated if the output module type is dependent upon the input module type. For example, suppose we wanted to create a functor that pairs up a value from one module with another value:
如果输出模块类型依赖于输入模块类型,函子类型语法将变得更加复杂。例如,假设我们想要创建一个函子,将一个模块中的一个值与另一个值配对:

module type T = sig
  type t
  val x : t
end

module Pair1 (M : T) = struct
  let p = (M.x, 1)
end
module type T = sig type t val x : t end
module Pair1 : functor (M : T) -> sig val p : M.t * int end

The type of Pair1 turns out to be:
Pair1 的类型是:

functor (M : T) -> sig val p : M.t * int end

So we could also write:
所以我们也可以这样写:

module type P1 = functor (M : T) -> sig val p : M.t * int end

module Pair1 : P1 = functor (M : T) -> struct
  let p = (M.x, 1)
end
module type P1 = functor (M : T) -> sig val p : M.t * int end
module Pair1 : P1

Module type P1 is the type of a functor that takes an input module named M of module type T, and returns an output module whose module type is given by the signature sig..end. Inside the signature, the name M is in scope. That’s why we can write M.t in it, thereby ensuring that the type of the first component of pair p is the type from the specific module M that is passed into Pair1, not any other module. For example, here are two different instantiations:
模块类型 P1 是函子的类型,它采用模块类型 T 的名为 M 的输入模块,并返回一个输出模块,其模块类型由下式给出签名 sig..end 。在签名内部,名称 M 在范围内。这就是为什么我们可以在其中写入 M.t ,从而确保对 p 的第一个组件的类型是传递的特定模块 M 的类型进入 Pair1 ,而不是任何其他模块。例如,这里有两个不同的实例:

module P0 = Pair1 (struct type t = int let x = 0 end)
module PA = Pair1 (struct type t = char let x = 'a' end)
module P0 : sig val p : int * int end
module PA : sig val p : char * int end

Note the difference between int and char in the resulting module types. It’s important that the output module type of Pair1 can distinguish those. And that’s why M has to be nameable on the right-hand side of the arrow in P1.
请注意生成的模块类型中 intchar 之间的差异。重要的是 Pair1 的输出模块类型可以区分它们。这就是为什么 M 必须在 P1 中箭头的右侧命名。

Note 笔记

Functor types are an example of an advanced programming language feature called dependent types, with which the type of an output is determined by the value of an input. That’s different than the normal case of a function, where it’s the output value that’s determined by the input value, and the output type is independent of the input value.
函子类型是称为依赖类型的高级编程语言功能的一个示例,其中输出的类型由输入的值确定。这与函数的正常情况不同,在正常情况下,函数的输出值由输入值决定,并且输出类型与输入值无关。

Dependent types enable type systems to express much more about the correctness of a program, but type checking and inference for dependent types is much more challenging. Practical dependent type systems are an active area of research. Perhaps someday they will become popular in mainstream languages.
依赖类型使类型系统能够更多地表达程序的正确性,但依赖类型的类型检查和推理更具挑战性。实用的依赖类型系统是一个活跃的研究领域。也许有一天它们会在主流语言中流行起来。

The module type of a functor’s actual argument need not be identical to the formal declared module type of the argument; it’s fine to be a subtype. For example, it’s fine to apply F below to either X or Z. The extra item in Z won’t cause any difficulty.
函子的实际参数的模块类型不需要与参数的正式声明的模块类型相同;作为一个亚型很好。例如,可以将下面的 F 应用于 XZZ 中的额外项目不会造成任何困难。

module F (M : sig val x : int end) = struct let y = M.x end
module X = struct let x = 0 end
module Z = struct let x = 0;; let z = 0 end
module FX = F (X)
module FZ = F (Z)
module F : functor (M : sig val x : int end) -> sig val y : int end
module X : sig val x : int end
module Z : sig val x : int val z : int end
module FX : sig val y : int end
module FZ : sig val y : int end

5.9.3. The Map Module
5.9.3. 映射 (Map) 模块 ¶

The standard library’s Map module implements a map (a binding from keys to values) using balanced binary trees. It uses functors in an important way. In this section, we study how to use it. You can see the implementation of that module on GitHub as well as its interface.
标准库的 Map 模块使用平衡二叉树实现映射(从键到值的绑定)。它以一种重要的方式使用函子。在本节中,我们将研究如何使用它。您可以在 GitHub 上查看该模块的实现及其接口。

The Map module defines a functor Make that creates a structure implementing a map over a particular type of keys. That type is the input structure to Make. The type of that input structure is Map.OrderedType, which are types that support a compare operation:
Map 模块定义了一个函子 Make ,它创建一个结构,实现特定类型键上的映射。该类型是 Make 的输入结构。该输入结构的类型是 Map.OrderedType ,它们是支持 compare 操作的类型:

module type OrderedType = sig
  type t
  val compare : t -> t -> int
end
module type OrderedType = sig type t val compare : t -> t -> int end

The Map module needs ordering, because balanced binary trees need to be able to compare keys to determine whether one is greater than another. The compare function’s specification is the same as that for the comparison argument to List.sort_uniq, which we previously discussed:
Map 模块需要排序,因为平衡二叉树需要能够比较键以确定一个是否大于另一个。 compare 函数的规范与 List.sort_uniq 的比较参数的规范相同,我们之前讨论过:

  • The comparison should return 0 if two keys are equal.
    如果两个键相等,则比较应返回 0

  • The comparison should return a strictly negative number if the first key is lesser than the second.
    如果第一个键小于第二个键,则比较应返回严格的负数。

  • The comparison should return a strictly positive number if the first key is greater than the second.
    如果第一个键大于第二个键,则比较应返回严格正数。

Note 笔记

Does that specification seem a little strange? Does it seem hard to remember when to return a negative vs. positive number? Why not define a variant instead?
这个规格看起来是不是有点奇怪?似乎很难记住何时返回负数和正数?为什么不定义一个变体呢?

type order = LT | EQ | GT
val compare : t -> t -> order

Alas, historically many languages have used comparison functions with similar specifications, such as the C standard library’s strcmp function. When comparing two integers, it does make the comparison easy: just perform a subtraction. It’s not necessarily so easy for other data types.
唉,历史上许多语言都使用过具有相似规范的比较函数,例如 C 标准库的 strcmp 函数。当比较两个整数时,它确实使比较变得容易:只需执行减法即可。对于其他数据类型来说不一定那么容易。

The output of Map.Make supports all the usual operations we would expect from a dictionary:
Map.Make 的输出支持我们期望从字典中获得的所有常见操作:

module type S = sig
  type key
  type 'a t
  val empty: 'a t
  val mem: key -> 'a t -> bool
  val add: key -> 'a -> 'a t -> 'a t
  val find: key -> 'a t -> 'a
  ...
end

The type variable 'a is the type of values in the map. So any particular map module created by Map.Make can handle only one type of key, but is not restricted to any particular type of value.
类型变量 'a 是映射中值的类型。因此 Map.Make 创建的任何特定映射模块只能处理一种类型的键,但不限于任何特定类型的值。

5.9.3.1. An Example Map
5.9.3.1. 映射示例 ¶

Here’s an example of using the Map.Make functor:
下面是使用 Map.Make 函子的示例:

module IntMap = Map.Make(Int)
module IntMap :
  sig
    type key = Int.t
    type 'a t = 'a Map.Make(Int).t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val mem : key -> 'a t -> bool
    val add : key -> 'a -> 'a t -> 'a t
    val update : key -> ('a option -> 'a option) -> 'a t -> 'a t
    val singleton : key -> 'a -> 'a t
    val remove : key -> 'a t -> 'a t
    val merge :
      (key -> 'a option -> 'b option -> 'c option) -> 'a t -> 'b t -> 'c t
    val union : (key -> 'a -> 'a -> 'a option) -> 'a t -> 'a t -> 'a t
    val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
    val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
    val iter : (key -> 'a -> unit) -> 'a t -> unit
    val fold : (key -> 'a -> 'b -> 'b) -> 'a t -> 'b -> 'b
    val for_all : (key -> 'a -> bool) -> 'a t -> bool
    val exists : (key -> 'a -> bool) -> 'a t -> bool
    val filter : (key -> 'a -> bool) -> 'a t -> 'a t
    val filter_map : (key -> 'a -> 'b option) -> 'a t -> 'b t
    val partition : (key -> 'a -> bool) -> 'a t -> 'a t * 'a t
    val cardinal : 'a t -> int
    val bindings : 'a t -> (key * 'a) list
    val min_binding : 'a t -> key * 'a
    val min_binding_opt : 'a t -> (key * 'a) option
    val max_binding : 'a t -> key * 'a
    val max_binding_opt : 'a t -> (key * 'a) option
    val choose : 'a t -> key * 'a
    val choose_opt : 'a t -> (key * 'a) option
    val split : key -> 'a t -> 'a t * 'a option * 'a t
    val find : key -> 'a t -> 'a
    val find_opt : key -> 'a t -> 'a option
    val find_first : (key -> bool) -> 'a t -> key * 'a
    val find_first_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val find_last : (key -> bool) -> 'a t -> key * 'a
    val find_last_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val map : ('a -> 'b) -> 'a t -> 'b t
    val mapi : (key -> 'a -> 'b) -> 'a t -> 'b t
    val to_seq : 'a t -> (key * 'a) Seq.t
    val to_rev_seq : 'a t -> (key * 'a) Seq.t
    val to_seq_from : key -> 'a t -> (key * 'a) Seq.t
    val add_seq : (key * 'a) Seq.t -> 'a t -> 'a t
    val of_seq : (key * 'a) Seq.t -> 'a t
  end

If you show that output, you’ll see the long module type of IntMap. The Int module is part of the standard library. Conveniently, it already defines the two items required by OrderedType, which are t and compare, with appropriate behaviors. The standard library also already defines modules for the other primitive types (String, etc.) that make it convenient to use any primitive type as a key.
如果显示该输出,您将看到 IntMap 的长模块类型。 Int 模块是标准库的一部分。方便的是,它已经定义了 OrderedType 所需的两个项目,即 tcompare ,并具有适当的行为。标准库还已经为其他基元类型( String 等)定义了模块,这使得可以方便地使用任何基元类型作为键。

Now let’s try out that map by mapping an int to a string:
现在让我们通过将 int 映射到 string 来尝试该映射:

open IntMap;;
let m1 = add 1 "one" empty
val m1 : string IntMap.t = <abstr>
find 1 m1
- : string = "one"
mem 42 m1
- : bool = false
find 42 m1
Exception: Not_found.
Raised at Stdlib__Map.Make.find in file "map.ml", line 137, characters 10-25
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150
bindings m1
- : (IntMap.key * string) list = [(1, "one")]

The same IntMap module allows us to map an int to a float:
相同的 IntMap 模块允许我们将 int 映射到 float

let m2 = add 1 1. empty
val m2 : float IntMap.t = <abstr>
bindings m2
- : (IntMap.key * float) list = [(1, 1.)]

But the keys must be int, not any other type:
但键必须是 int ,而不是任何其他类型:

let m3 = add true "one" empty
File "[26]", line 1, characters 13-17:
1 | let m3 = add true "one" empty
                 ^^^^
Error: This expression has type bool but an expression was expected of type
         IntMap.key = int

That’s because the IntMap module was specifically created for keys that are integers and ordered accordingly. Again, order is crucial, because the underlying data structure is a binary search tree, which requires key comparisons to figure out where in the tree to store a key. You can even see that in the standard library source code (v4.12), of which the following is a lightly-edited extract:
这是因为 IntMap 模块是专门为整数键创建的,并相应地排序。同样,顺序至关重要,因为底层数据结构是二叉搜索树,它需要进行键比较才能找出树中存储键的位置。您甚至可以在标准库源代码(v4.12)中看到这一点,其中以下是经过轻微编辑的摘录:

module Make (Ord : OrderedType) = struct
  type key = Ord.t

  type 'a t =
    | Empty
    | Node of {l : 'a t; v : key; d : 'a; r : 'a t; h : int}
      (** left subtree, key, value/data, right subtree, height of node *)

  let empty = Empty

  let rec mem x = function
    | Empty -> false
    | Node {l, v, r} ->
        let c = Ord.compare x v in
        c = 0 || mem x (if c < 0 then l else r)
  ...
end

The key type is defined to be a synonym for the type t inside Ord, so key values are comparable using Ord.compare. The mem function uses that to compare keys and decide whether to recurse on the left subtree or right subtree.
key 类型被定义为 Ord 内类型 t 的同义词,因此 key 值可以使用 Ord.compare 函数使用它来比较键并决定是在左子树还是右子树上递归。

Note how the implementor of Map had a tricky problem to solve: balanced binary search trees require a way to compare keys, but the implementor can’t know in advance all the different types of keys that a client of the data structure will want to use. And each type of key might need its own comparison function. Although Stdlib.compare can be used to compare any two values of the same type, the result it returns isn’t necessarily what a client will want. For example, it’s not guaranteed to sort names in the way we wanted above.
请注意 Map 的实现者如何解决一个棘手的问题:平衡二叉搜索树需要一种比较键的方法,但实现者无法提前知道客户端的所有不同类型的键。数据结构就会想用。并且每种类型的密钥可能需要其自己的比较功能。虽然 Stdlib.compare 可用于比较相同类型的任意两个值,但它返回的结果不一定是客户端想要的结果。例如,不能保证按照我们上面想要的方式对名称进行排序。

So the implementor of Map used a functor to solve their problem. They parameterized on a module that bundles together the type of keys with a function that can be used to compare them. It’s the client’s responsibility to implement that module.
因此 Map 的实现者使用函子来解决他们的问题。他们对一个模块进行参数化,该模块将键的类型与可用于比较它们的函数捆绑在一起。客户有责任实施该模块。

The Java Collections Framework solves a similar problem in the TreeMap class, which has a constructor that takes a Comparator. There, the client has the responsibility of implementing a class for comparisons, rather than a structure. Though the language features are different, the idea is the same.
Java 集合框架解决了 TreeMap 类中的类似问题,该类具有一个采用 Comparator 的构造函数。在那里,客户端有责任实现一个用于比较的类,而不是一个结构。虽然语言特点不同,但思想是一样的。

5.9.3.2. Maps with Custom Key Types
5.9.3.2. 具有自定义键类型的映射 ¶

When the type of a key becomes complicated, we might want to write our own custom comparison function. For example, suppose we want a map in which keys are records representing names, and in which names are sorted alphabetically by last name then by first name. In the code below, we provide a module Name that can compare records that way:
当键的类型变得复杂时,我们可能想编写自己的自定义比较函数。例如,假设我们想要一个映射,其中键是代表姓名的记录,并且其中姓名按姓氏字母顺序排序,然后按名字排序。在下面的代码中,我们提供了一个模块 Name ,可以通过这种方式比较记录:

type name = {first : string; last : string}

module Name = struct
  type t = name
  let compare { first = first1; last = last1 } { first = first2; last = last2 }
      =
    match String.compare last1 last2 with
    | 0 -> String.compare first1 first2
    | c -> c
end
type name = { first : string; last : string; }
module Name : sig type t = name val compare : name -> name -> int end

The Name module can be used as input to Map.Make because it satisfies the Map.OrderedType signature:
Name 模块可以用作 Map.Make 的输入,因为它满足 Map.OrderedType 签名:

module NameMap = Map.Make (Name)
module NameMap :
  sig
    type key = Name.t
    type 'a t = 'a Map.Make(Name).t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val mem : key -> 'a t -> bool
    val add : key -> 'a -> 'a t -> 'a t
    val update : key -> ('a option -> 'a option) -> 'a t -> 'a t
    val singleton : key -> 'a -> 'a t
    val remove : key -> 'a t -> 'a t
    val merge :
      (key -> 'a option -> 'b option -> 'c option) -> 'a t -> 'b t -> 'c t
    val union : (key -> 'a -> 'a -> 'a option) -> 'a t -> 'a t -> 'a t
    val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
    val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
    val iter : (key -> 'a -> unit) -> 'a t -> unit
    val fold : (key -> 'a -> 'b -> 'b) -> 'a t -> 'b -> 'b
    val for_all : (key -> 'a -> bool) -> 'a t -> bool
    val exists : (key -> 'a -> bool) -> 'a t -> bool
    val filter : (key -> 'a -> bool) -> 'a t -> 'a t
    val filter_map : (key -> 'a -> 'b option) -> 'a t -> 'b t
    val partition : (key -> 'a -> bool) -> 'a t -> 'a t * 'a t
    val cardinal : 'a t -> int
    val bindings : 'a t -> (key * 'a) list
    val min_binding : 'a t -> key * 'a
    val min_binding_opt : 'a t -> (key * 'a) option
    val max_binding : 'a t -> key * 'a
    val max_binding_opt : 'a t -> (key * 'a) option
    val choose : 'a t -> key * 'a
    val choose_opt : 'a t -> (key * 'a) option
    val split : key -> 'a t -> 'a t * 'a option * 'a t
    val find : key -> 'a t -> 'a
    val find_opt : key -> 'a t -> 'a option
    val find_first : (key -> bool) -> 'a t -> key * 'a
    val find_first_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val find_last : (key -> bool) -> 'a t -> key * 'a
    val find_last_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val map : ('a -> 'b) -> 'a t -> 'b t
    val mapi : (key -> 'a -> 'b) -> 'a t -> 'b t
    val to_seq : 'a t -> (key * 'a) Seq.t
    val to_rev_seq : 'a t -> (key * 'a) Seq.t
    val to_seq_from : key -> 'a t -> (key * 'a) Seq.t
    val add_seq : (key * 'a) Seq.t -> 'a t -> 'a t
    val of_seq : (key * 'a) Seq.t -> 'a t
  end

Now we could use that map to associate names with birth years:
现在我们可以使用该映射将姓名与出生年份关联起来:

let k1 = {last = "Kardashian"; first = "Kourtney"}
let k2 = {last = "Kardashian"; first = "Kimberly"}
let k3 = {last = "Kardashian"; first = "Khloe"}

let nm =
  NameMap.(empty |> add k1 1979 |> add k2 1980 |> add k3 1984)

let lst = NameMap.bindings nm
val k1 : name = {first = "Kourtney"; last = "Kardashian"}
val k2 : name = {first = "Kimberly"; last = "Kardashian"}
val k3 : name = {first = "Khloe"; last = "Kardashian"}
val nm : int NameMap.t = <abstr>
val lst : (NameMap.key * int) list =
  [({first = "Khloe"; last = "Kardashian"}, 1984);
   ({first = "Kimberly"; last = "Kardashian"}, 1980);
   ({first = "Kourtney"; last = "Kardashian"}, 1979)]

Note how the order of keys in that list is not the same as the order in which we added them. The list is sorted according to the Name.compare function we wrote. Several of the other functions in the Map.S signature will also process map bindings in that sorted order—for example, map, fold, and iter.
请注意该列表中键的顺序与我们添加它们的顺序不同。该列表根据我们编写的 Name.compare 函数进行排序。 Map.S 签名中的其他几个函数也将按排序顺序处理映射绑定,例如 mapfolditer

5.9.3.3. How Map Uses Module Type Constraints
5.9.3.3. 映射模块 Map 是如何使用模块类型约束的 ¶

In the standard library’s map.mli interface, the specification for Map.Make is:
在标准库的 map.mli 接口中, Map.Make 的规范是:

module Make (Ord : OrderedType) : S with type key = Ord.t

The with constraint there is crucial. Recall that type constraints specialize a module type. Here, S with type key = Ord.t specializes S to expose the equality of S.key and Ord.t. In other words, the type of keys is the ordered type.
其中的 with 约束至关重要。回想一下,类型约束专门针对模块类型。在这里, S with type key = Ord.t 专门使用 S 来公开 S.keyOrd.t 的相等性。换句话说,键的类型是有序类型。

You can see the effect of that sharing constraint by looking at the module type of our IntMap example from before. The sharing constraint is what caused the = Int.t to be present:
您可以通过查看之前的 IntMap 示例的模块类型来了解该共享约束的效果。共享约束是导致 = Int.t 出现的原因:

module IntMap : sig
  type key = Int.t
  ...
end

And the Int module contains this line:
Int 模块包含这一行:

type t = int

So IntMap.key = Int.t = int, which is exactly why we’re allowed to pass an int to the add and mem functions of IntMap.
所以 IntMap.key = Int.t = int ,这正是我们被允许将 int 传递给 IntMapaddmem 函数的原因。

Without the type constraint, type key would have remained abstract. We can simulate that by adding a module type annotation of Map.S, thereby resealing the module at that type without exposing the equality:
如果没有类型约束,类型 key 将保持抽象状态。我们可以通过添加 Map.S 的模块类型注释来模拟这一点,从而在不暴露等式的情况下重新封装该类型的模块:

module UnusableMap = (IntMap : Map.S);;
module UnusableMap : Map.S

Now it’s impossible to add a binding to the map:
现在不可能向地图添加绑定:

let m = UnusableMap.(empty |> add 0 "zero")
File "[31]", line 1, characters 34-35:
1 | let m = UnusableMap.(empty |> add 0 "zero")
                                      ^
Error: This expression has type int but an expression was expected of type
         UnusableMap.key

This kind of use case is why module type constraints are quite important in effective programming with the OCaml module system. Often it is necessary to specialize the output type of a functor to show a relationship between a type in it and a type in one of the functor’s inputs. Thinking through exactly what constraint is necessary can be challenging, though!
这种用例就是为什么模块类型约束对于使用 OCaml 模块系统进行有效编程非常重要。通常,有必要专门化函子的输出类型,以显示其中的类型与函子输入之一的类型之间的关系。然而,准确地思考什么约束是必要的可能具有挑战性!

5.9.4. Using Functors 5.9.4. 使用函子 ¶

With Map we saw one use case for functors: producing a data structure that was parameterized on a client-provided ordering. Here are two more use cases.
通过 Map ,我们看到了函子的一个用例:生成根据客户端提供的排序进行参数化的数据结构。这里还有两个用例。

5.9.4.1. Test Suites 5.9.4.1. 测试套件 ¶

Here are two implementations of a stack:
下面是堆栈的两种实现:

exception Empty

module type Stack = sig
  type 'a t
  val empty : 'a t
  val push : 'a -> 'a t -> 'a t
  val peek : 'a t -> 'a
  val pop : 'a t -> 'a t
end

module ListStack = struct
  type 'a t = 'a list
  let empty = []
  let push = List.cons
  let peek = function [] -> raise Empty | x :: _ -> x
  let pop = function [] -> raise Empty | _ :: s -> s
end

module VariantStack = struct
  type 'a t = E | S of 'a * 'a t
  let empty = E
  let push x s = S (x, s)
  let peek = function E -> raise Empty | S (x, _) -> x
  let pop = function E -> raise Empty | S (_, s) -> s
end
exception Empty
module type Stack =
  sig
    type 'a t
    val empty : 'a t
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val pop : 'a t -> 'a t
  end
module ListStack :
  sig
    type 'a t = 'a list
    val empty : 'a list
    val push : 'a -> 'a list -> 'a list
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end
module VariantStack :
  sig
    type 'a t = E | S of 'a * 'a t
    val empty : 'a t
    val push : 'a -> 'a t -> 'a t
    val peek : 'a t -> 'a
    val pop : 'a t -> 'a t
  end

Suppose we wanted to write an OUnit test for ListStack:
假设我们想为 ListStack 编写一个 OUnit 测试:

let test = "peek (push x empty) = x" >:: fun _ ->
  assert_equal 1 ListStack.(empty |> push 1 |> peek)

Unfortunately, to test a VariantStack, we’d have to duplicate that code:
不幸的是,要测试 VariantStack ,我们必须复制该代码:

let test' = "peek (push x empty) = x" >:: fun _ ->
  assert_equal 1 VariantStack.(empty |> push 1 |> peek)

And if we had other stack implementations, we’d have to duplicate the test for them, too. That’s not so horrible to contemplate if it’s just one test case for a couple implementations, but if it’s hundreds of tests for even a couple implementations, that’s just too much duplication to be good software engineering.
如果我们有其他堆栈实现,我们也必须为它们重复测试。如果它只是几个实现的一个测试用例,那么考虑起来并没有那么可怕,但如果它甚至是几个实现的数百个测试,那么对于优秀的软件工程来说,重复太多了。

Functors offer a better solution. We can write a functor that is parameterized on the stack implementation, and produces the test for that implementation:
函子提供了更好的解决方案。我们可以编写一个在堆栈实现上参数化的函子,并为该实现生成测试:

module StackTester (S : Stack) = struct
  let tests = [
    "peek (push x empty) = x" >:: fun _ ->
      assert_equal 1 S.(empty |> push 1 |> peek)
  ]
end

module ListStackTester = StackTester (ListStack)
module VariantStackTester = StackTester (VariantStack)

let all_tests = List.flatten [
  ListStackTester.tests;
  VariantStackTester.tests
]

Now whenever we invent a new test we add it to StackTester, and it automatically gets run on both stack implementations. Nice!
现在,每当我们发明一个新测试时,我们都会将其添加到 StackTester 中,并且它会自动在两个堆栈实现上运行。好的!

There is still some objectionable code duplication, though, in that we have to write two lines of code per implementation. We can eliminate that duplication through the use of first-class modules:
不过,仍然存在一些令人反感的代码重复,因为我们必须为每个实现编写两行代码。我们可以通过使用一流的模块来消除重复:

let stacks = [ (module ListStack : Stack); (module VariantStack) ]

let all_tests =
  let tests m =
    let module S = (val m : Stack) in
    let module T = StackTester (S) in
    T.tests
  in
  let open List in
  stacks |> map tests |> flatten

Now it suffices just to add the newest stack implementation to the stacks list. Nicer!
现在只需将最新的堆栈实现添加到 stacks 列表中即可。更好了!

5.9.4.2. Extending Multiple Modules
5.9.4.2. 扩展多个模块 ¶

Earlier, we tried to add a function of_list to both ListSet and UniqListSet without having any duplicated code, but we didn’t totally succeed. Now let’s really do it right.
早些时候,我们尝试向 ListSetUniqListSet 添加函数 of_list ,而不使用任何重复的代码,但我们并没有完全成功。现在让我们真正做对吧。

The problem we had earlier was that we needed to parameterize the implementation of of_list on the add function and empty value in the set module. We can accomplish that parameterization with a functor:
我们之前遇到的问题是,我们需要参数化 set 模块中 add 函数和 empty 值上 of_list 的实现。我们可以使用函子来完成参数化:

module type Set = sig
  type 'a t
  val empty : 'a t
  val mem : 'a -> 'a t -> bool
  val add : 'a -> 'a t -> 'a t
  val elements : 'a t -> 'a list
end

module SetOfList (S : Set) = struct
  let of_list lst = List.fold_right S.add lst S.empty
end

Notice how the functor, in its body, uses S.add. It takes the implementation of add from S and uses it to implement of_list (and the same for empty), thus solving the exact problem we had before when we tried to use includes.
请注意函子在其主体中如何使用 S.add 。它从 S 获取 add 的实现,并使用它来实现 of_list (与 empty 相同),从而解决了确切的问题我们之前尝试使用包含时遇到的问题。

When we apply SetOfList to our set implementations, we get modules containing an of_list function for each implementation:
当我们将 SetOfList 应用于我们的集合实现时,我们会得到每个实现都包含一个 of_list 函数的模块:

module ListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add = List.cons
  let elements s = List.sort_uniq Stdlib.compare s
end

module UniqListSet : Set = struct
  (** All values in the list must be unique. *)
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add x s = if mem x s then s else x :: s
  let elements = Fun.id
end
module OfList = SetOfList (ListSet)
module UniqOfList = SetOfList (UniqListSet)
module OfList : sig val of_list : 'a list -> 'a ListSet.t end
module UniqOfList : sig val of_list : 'a list -> 'a UniqListSet.t end

The functor has enabled the code reuse we couldn’t get before: we now can implement a single of_list function and from it derive implementations for two different sets.
函子实现了我们以前无法实现的代码重用:我们现在可以实现单个 of_list 函数,并从中派生两个不同集的实现。

But that’s the only function those two modules contain. Really what we want is a full set implementation that also contains the of_list function. We can get that by combining includes with functors:
但这是这两个模块包含的唯一功能。我们真正想要的是还包含 of_list 函数的全套实现。我们可以通过将 include 与函子结合起来得到:

module SetWithOfList (S : Set) = struct
  include S
  let of_list lst = List.fold_right S.add lst S.empty
end
module SetWithOfList :
  functor (S : Set) ->
    sig
      type 'a t = 'a S.t
      val empty : 'a t
      val mem : 'a -> 'a t -> bool
      val add : 'a -> 'a t -> 'a t
      val elements : 'a t -> 'a list
      val of_list : 'a list -> 'a S.t
    end

That functor takes a set as input, and produces a module that contains everything from that set (because of the include) as well as a new function of_list.
该函子接受一个集合作为输入,并生成一个包含该集合中所有内容的模块(因为 include )以及一个新函数 of_list

When we apply the functor, we get a very nice set module:
当我们应用函子时,我们得到一个非常好的集合模块:

module SetL = SetWithOfList (ListSet)
module UniqSetL = SetWithOfList (UniqListSet)
module SetL :
  sig
    type 'a t = 'a ListSet.t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a ListSet.t
  end
module UniqSetL :
  sig
    type 'a t = 'a UniqListSet.t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val elements : 'a t -> 'a list
    val of_list : 'a list -> 'a UniqListSet.t
  end

Notice how the output structure records the fact that its type t is the same type as the type t in its input structure. They share it because of the include.
请注意输出结构如何记录其类型 t 与其输入结构中的类型 t 类型相同的事实。他们因为 include 而分享它。

Stepping back, what we just did bears more than a passing resemblance to class extension in Java. We created a base module and extended its functionality with new code while preserving its old functionality. But whereas class extension necessitates that the newly extended class is a subtype of the old, and that it still has all the old functionality, OCaml functors are more fine-grained in what they can accomplish. We can choose whether they include the old functionality. And no subtyping relationships are necessarily involved. Moreover, the functor we wrote can be used to extend any set implementation with of_list, whereas class extension applies to just a single base class. There are ways of achieving something similar in object-oriented languages with mixins, which enable a class to re-use functionality from other classes without necessitating the complication of multiple inheritance.
退一步来说,我们刚才所做的与 Java 中的类扩展非常相似。我们创建了一个基本模块,并使用新代码扩展了其功能,同时保留了其旧功能。但是,虽然类扩展要求新扩展的类是旧类的子类型,并且它仍然具有所有旧功能,但 OCaml 函子在它们可以完成的任务方面更加细粒度。我们可以选择它们是否包含旧功能。并且不一定涉及子类型关系。此外,我们编写的函子可用于使用 of_list 扩展任何集合实现,而类扩展仅适用于单个基类。在面向对象语言中,有一些方法可以通过 mixins 实现类似的功能,使一个类能够重用其他类的功能,而无需复杂的多重继承。

5.10. Summary 5.10. 小结 ¶

The OCaml module system provides mechanisms for modularity that provide the similar capabilities as mechanisms you will have seen in other languages. But seeing those mechanisms appear in different ways is hopefully helping you understand them better. OCaml abstract types and signatures, for example, provide a mechanism for abstraction that resembles Java visibility modifiers and interfaces. Seeing the same idea embodied in two different languages, but expressed in rather different ways, will hopefully help you recognize that idea when you encounter it in other languages in the future.
OCaml 模块系统提供了模块化机制,这些机制提供了与您在其他语言中看到的机制类似的功能。但看到这些机制以不同的方式出现有望帮助您更好地理解它们。例如,OCaml 抽象类型和签名提供了一种类似于 Java 可见性修饰符和接口的抽象机制。看到相同的想法在两种不同的语言中体现,但以相当不同的方式表达,希望能帮助您在将来在其他语言中遇到该想法时识别该想法。

Moreover, the idea that a type could be abstract is a foundational notion in programming language design. The OCaml module system makes that idea brutally apparent. Other languages like Java obscure it a bit by coupling it together with many other features all at once. There’s a sense in which every Java class implicitly defines an abstract type (actually, four abstract types that are related by subtyping, one for each visibility modifier [public, protected, private, and default]), and all the methods of the class are functions on that abstract type.
此外,类型可以是抽象的想法是编程语言设计中的基本概念。 OCaml 模块系统使这个想法变得非常明显。其他语言(例如 Java)通过将其与许多其他功能同时耦合在一起而稍微掩盖了它。从某种意义上说,每个 Java 类都隐式定义了一个抽象类型(实际上,通过子类型相关的四种抽象类型,每个可见性修饰符都有一个〔 publicprotectedprivatedefault 〕),并且该类的所有方法都是该抽象类型上的函数。

Functors are an advanced language feature in OCaml that might seem mysterious at first. If so, keep in mind: they’re really just a kind of function that takes a structure as input and returns a structure as output. The reason they don’t behave quite like normal OCaml functions is that structures are not first-class values in OCaml: you can’t write regular functions that take a structure as input or return a structure as output. But functors can do just that.
函子是 OCaml 中的一项高级语言功能,乍一看似乎很神秘。如果是这样,请记住:它们实际上只是一种将结构作为输入并返回结构作为输出的函数。它们的行为与普通 OCaml 函数不太一样的原因是,结构在 OCaml 中不是一等值:您无法编写将结构作为输入或返回结构作为输出的常规函数​​。但函子可以做到这一点。

Functors and includes enable code reuse. The kinds of code reuse that object-oriented features enable can also be achieved with functors and include. That’s not to say that functors and includes are exactly equivalent to those object-oriented features: some kinds of code reuse might be easier to achieve with one set of features than the other.
函子和包含启用代码重用。面向对象功能所支持的代码重用类型也可以通过仿函数和包含来实现。这并不是说函子和包含完全等同于那些面向对象的功能:使用一组功能可能比使用另一组功能更容易实现某些类型的代码重用。

One way to think about this might be that class extension is a very limited, but very useful, combination of functors and includes. Extending a class is like writing a functor that takes the base class as input, includes it, then adds new functions. But functors provide more general capability than class extension, because they can compute arbitrary functions of their input structure, rather than being limited to just certain kinds of extension.
思考这个问题的一种方法可能是类扩展是一个非常有限但非常有用的函子和包含的组合。扩展类就像编写一个仿函数,它将基类作为输入,包含它,然后添加新函数。但函子提供了比类扩展更通用的功能,因为它们可以计算其输入结构的任意函数,而不是仅限于某些类型的扩展。

Perhaps the most important idea to get out of studying the OCaml module system is an appreciation for the aspects of modularity that transcend any given language: namespaces, abstraction, and code reuse. Having seen those ideas in a couple very different languages, you’re equipped to recognize them more clearly in the next language you learn.
也许从研究 OCaml 模块系统中得到的最重要的想法是对超越任何给定语言的模块化方面的欣赏:命名空间、抽象和代码重用。在几种截然不同的语言中看到这些想法后,您就可以在学习的下一种语言中更清楚地识别它们。

5.10.1. Terms and Concepts
5.10.1. 术语和概念 ¶

  • abstract type 抽象类型

  • abstraction 抽象

  • client 客户

  • code reuse 代码重用

  • compilation unit 编译单元

  • declaration 宣言

  • definition 定义

  • encapsulation 封装

  • ephemeral data structure 临时数据结构

  • functional data structure
    函数式数据结构

  • functor 函子

  • implementation 执行

  • implementer 实施者

  • include 包括

  • information hiding 信息隐藏

  • interface 界面

  • local reasoning 局部推理

  • maintainability 可维护性

  • maps 地图

  • modular programming 模块化编程

  • modularity 模块化

  • module 模块

  • module type 模块类型

  • namespace 名称空间

  • open 打开

  • parameterized structure 参数化结构

  • persistent data structure
    持久数据结构

  • representation type 表示类型

  • scope 范围

  • sealed 密封

  • set representations 设定表示

  • sharing constraints 共享限制

  • signature 签名

  • signature matching 签名匹配

  • specification 规格

  • structure 结构

5.10.2. Further Reading
5.10.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapters 11, 12, and 13
    Objective Caml 简介,第 11、12 和 13 章

  • OCaml from the Very Beginning, chapter 16
    OCaml 从头开始​​,第 16 章

  • Real World OCaml, chapters 4, 9, and 10
    现实世界 OCaml,第 4、9 和 10 章

  • Purely Functional Data Structures, chapters 1 and 2, by Chris Okasaki.
    纯函数式数据结构,第 1 章和第 2 章,作者:Chris Okasaki。

  • “Design Considerations for ML-Style Module Systems” by Robert Harper and Benjamin C. Pierce, chapter 8 of Advanced Topics in Types and Programming Languages, ed. Benjamin C. Pierce, MIT Press, 2005. An advanced treatment of the static semantics of modules.
    Robert Harper 和 Benjamin C. Pierce 撰写的“ML 风格模块系统的设计注意事项”,《类型和编程语言高级主题》第 8 章,编辑。 Benjamin C. Pierce,麻省理工学院出版社,2005 年。模块静态语义的高级处理。

5.11. Exercises 5.11. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: complex synonym [★]
练习:复杂同义词[★]

Here is a module type for complex numbers, which have a real and imaginary component:
这是复数的模块类型,它具有实部和虚部:

module type ComplexSig = sig
  val zero : float * float
  val add : float * float -> float * float -> float * float
end

Improve that code by adding type t = float * float. Show how the signature can be written more tersely because of the type synonym.
通过添加 type t = float * float 改进该代码。展示如何由于类型同义词而将签名写得更简洁。


Exercise: complex encapsulation [★★]
练习:复杂封装[★★]

Here is a module for the module type from the previous exercise:
这是上一个练习中的模块类型的模块:

module Complex : ComplexSig = struct
  type t = float * float
  let zero = (0., 0.)
  let add (r1, i1) (r2, i2) = r1 +. r2, i1 +. i2
end

Investigate what happens if you make the following changes (each independently), and explain why any errors arise:
研究如果进行以下更改(每项独立更改)会发生什么情况,并解释出现错误的原因:

  • remove zero from the structure
    从结构中删除 zero

  • remove add from the signature
    从签名中删除 add

  • change zero in the structure to let zero = 0, 0
    将结构中的 zero 更改为 let zero = 0, 0


Exercise: big list queue [★★]
练习:大列表队列 [★★]

Use the following code to create ListQueue of exponentially increasing length: 10, 100, 1000, etc. How big of a queue can you create before there is a noticeable delay? How big until there’s a delay of at least 10 seconds? (Note: you can abort utop computations with Ctrl-C.)
使用以下代码创建长度呈指数增长的 ListQueue :10、100、1000 等。在出现明显延迟之前,您可以创建多大的队列?延迟至少 10 秒之前有多大? (注意:您可以使用 Ctrl-C 中止 utop 计算。)

(** Creates a ListQueue filled with [n] elements. *)
let fill_listqueue n =
  let rec loop n q =
    if n = 0 then q
    else loop (n - 1) (ListQueue.enqueue n q) in
  loop n ListQueue.empty

Exercise: big batched queue [★★]
练习:大批量队列 [★★]

Use the following function to create BatchedQueue of exponentially increasing length:
使用以下函数创建长度呈指数增长的 BatchedQueue

let fill_batchedqueue n =
  let rec loop n q =
    if n = 0 then q
    else loop (n - 1) (BatchedQueue.enqueue n q) in
  loop n BatchedQueue.empty

Now how big of a queue can you create before there’s a delay of at least 10 seconds?
现在,在至少 10 秒的延迟之前,您可以创建多大的队列?


Exercise: queue efficiency [★★★]
练习:排队效率[★★★]

Compare the implementations of enqueue in ListQueue vs. BatchedQueue. Explain in your own words why the efficiency of ListQueue.enqueue is linear time in the length of the queue. Hint: consider the @ operator. Then explain why adding n elements to the queue takes time that is quadratic in n.
比较 ListQueueBatchedQueueenqueue 的实现。用你自己的话解释为什么 ListQueue.enqueue 的效率是队列长度的线性时间。提示:考虑 @ 运算符。然后解释为什么将 n 元素添加到队列中所需的时间是 n 的二次方。

Now consider BatchedQueue.enqueue. Suppose that the queue is in a state where it has never had any elements dequeued. Explain in your own words why BatchedQueue.enqueue is constant time. Then explain why adding n elements to the queue takes time that is linear in n.
现在考虑 BatchedQueue.enqueue 。假设队列处于从未有任何元素出队的状态。用你自己的话解释为什么 BatchedQueue.enqueue 是常数时间。然后解释为什么将 n 元素添加到队列中所需的时间与 n 呈线性关系。


Exercise: binary search tree map [★★★★]
练习:二叉搜索树映射[★★★★]

Write a module BstMap that implements the Map module type using a binary search tree type. Binary trees were covered earlier when we discussed algebraic data types. A binary search tree (BST) is a binary tree that obeys the following BST Invariant:
编写一个模块 BstMap ,使用二叉搜索树类型实现 Map 模块类型。之前我们讨论代数数据类型时已经介绍过二叉树。二叉搜索树 (BST) 是遵循以下 BST 不变量的二叉树:

For any node n, every node in the left subtree of n has a value less than n’s value, and every node in the right subtree of n has a value greater than n’s value.
对于任意节点n,n的左子树中的每个节点的值都小于n的值,并且n的右子树中的每个节点的值都大于n的值。

Your nodes should store pairs of keys and values. The keys should be ordered by the BST Invariant. Based on that invariant, you will always know whether to look left or right in a tree to find a particular key.
您的节点应该存储键和值对。键应按 BST 不变量排序。基于这个不变量,您将始终知道是在树中向左还是向右查找以找到特定的键。


Exercise: fraction [★★★] 练习:分数[★★★]

Write a module that implements the Fraction module type below:
编写一个实现以下 Fraction 模块类型的模块:

module type Fraction = sig
  (* A fraction is a rational number p/q, where q != 0.*)
  type t

  (** [make n d] is n/d. Requires d != 0. *)
  val make : int -> int -> t

  val numerator : t -> int
  val denominator : t -> int
  val to_string : t -> string
  val to_float : t -> float

  val add : t -> t -> t
  val mul : t -> t -> t
end

Exercise: fraction reduced [★★★]
练习:分数减少[★★★]

Modify your implementation of Fraction to ensure these invariants hold for every value v of type t that is returned from make, add, and mul:
修改 Fraction 的实现,以确保这些不变量适用于从 makeadd 类型的每个值 vmul

  1. v is in reduced form
    v 是简化形式

  2. the denominator of v is positive
    v 的分母为正

For the first invariant, you might find this implementation of Euclid’s algorithm to be helpful:
对于第一个不变量,您可能会发现欧几里得算法的实现很有帮助:

(** [gcd x y] is the greatest common divisor of [x] and [y].
    Requires: [x] and [y] are positive. *)
let rec gcd x y =
  if x = 0 then y
  else if (x < y) then gcd (y - x) x
  else gcd y (x - y)

Exercise: make char map [★]
练习:制作字符映射模块[★]

To create a standard library map, we first have to use the Map.Make functor to produce a module that is specialized for the type of keys we want. Type the following in utop:
要创建标准库映射,我们首先必须使用 Map.Make 函子来生成专门用于我们想要的键类型的模块。在 utop 中输入以下内容:

# module CharMap = Map.Make(Char);;

The output tells you that a new module named CharMap has been defined, and it gives you a signature for it. Find the values empty, add, and remove in that signature. Explain their types in your own words.
输出告诉您已经定义了一个名为 CharMap 的新模块,并为其提供了一个签名。在该签名中查找值 emptyaddremove 。用你自己的话解释它们的类型。


Exercise: char ordered [★]
练习:字符模块有序[★]

The Map.Make functor requires its input module to match the Map.OrderedType signature. Look at that signature as well as the signature for the Char module. Explain in your own words why we are allowed to pass Char as an argument to Map.Make.
Map.Make 仿函数要求其输入模块与 Map.OrderedType 签名匹配。查看该签名以及 Char 模块的签名。用你自己的话解释为什么我们可以将 Char 作为参数传递给 Map.Make


Exercise: use char map [★★]
练习:使用字符映射[★★]

Using the CharMap you just made, create a map that contains the following bindings:
使用您刚刚创建的 CharMap ,创建一个包含以下绑定的映射:

  • 'A' maps to "Alpha"
    'A' 映射到 "Alpha"

  • 'E' maps to "Echo"
    'E' 映射到 "Echo"

  • 'S' maps to "Sierra"
    'S' 映射到 "Sierra"

  • 'V' maps to "Victor"
    'V' 映射到 "Victor"

Use CharMap.find to find the binding for 'E'.
使用 CharMap.find 查找 'E' 的绑定。

Now remove the binding for 'A'. Use CharMap.mem to find whether 'A' is still bound.
现在删除 'A' 的绑定。使用 CharMap.mem 查找 'A' 是否仍处于绑定状态。

Use the function CharMap.bindings to convert your map into an association list.
使用函数 CharMap.bindings 将映射转换为关联列表。


Exercise: bindings [★★] 练习:绑定[★★]

Investigate the documentation of the Map.S signature to find the specification of bindings. Which of these expressions will return the same association list?
调查 Map.S 签名的文档以查找 bindings 的规范。以下哪个表达式将返回相同的关联列表?

  1. CharMap.(empty |> add 'x' 0 |> add 'y' 1 |> bindings)

  2. CharMap.(empty |> add 'y' 1 |> add 'x' 0 |> bindings)

  3. CharMap.(empty |> add 'x' 2 |> add 'y' 1 |> remove 'x' |> add 'x' 0 |> bindings)

Check your answer in utop.
在 utop 中检查你的答案。


Exercise: date order [★★]
练习:日期规则[★★]

Here is a type for dates:
这是日期类型:

type date = {month : int; day : int}

For example, March 31st would be represented as {month = 3; day = 31}. Our goal in the next few exercises is to implement a map whose keys have type date.
例如,3 月 31 日将表示为 {month = 3; day = 31} 。在接下来的几个练习中,我们的目标是实现一个键类型为 date 的映射。

Obviously it’s possible to represent invalid dates with type date—for example, { month=6; day=50 } would be June 50th, which is not a real date. The behavior of your code in the exercises below is unspecified for invalid dates.
显然,可以用 date 类型表示无效日期 - 例如, { month=6; day=50 } 是 6 月 50 日,这不是一个真实的日期。以下练习中的代码行为未指定无效日期。

To create a map over dates, we need a module that we can pass as input to Map.Make. That module will need to match the Map.OrderedType signature. Create such a module. Here is some code to get you started:
要创建日期映射,我们需要一个可以作为输入传递给 Map.Make 的模块。该模块需要匹配 Map.OrderedType 签名。创建这样一个模块。下面是一些可以帮助您入门的代码:

module Date = struct
  type t = date
  let compare ...
end

Recall the specification of compare in Map.OrderedType as you write your Date.compare function.
在编写 Date.compare 函数时,回想一下 Map.OrderedTypecompare 的规范。


Exercise: calendar [★★] 练习:日历[★★]

Use the Map.Make functor with your Date module to create a DateMap module. Then define a calendar type as follows:
Map.Make 函子与 Date 模块一起使用来创建 DateMap 模块。然后定义一个 calendar 类型,如下所示:

type calendar = string DateMap.t

The idea is that calendar maps a date to the name of an event occurring on that date.
这个想法是 calendardate 映射到该日期发生的事件的名称。

Using the functions in the DateMap module, create a calendar with a few entries in it, such as birthdays or anniversaries.
使用 DateMap 模块中的函数创建一个包含一些条目的日历,例如生日或周年纪念日。


Exercise: print calendar [★★]
练习:打印日历[★★]

Write a function print_calendar : calendar -> unit that prints each entry in a calendar in a format similar to the inspiring examples in the previous exercise. Hint: use DateMap.iter, which is documented in the Map.S signature.
编写一个函数 print_calendar : calendar -> unit ,以类似于上一个练习中鼓舞人心的示例的格式打印日历中的每个条目。提示:使用 DateMap.iter ,它记录在 Map.S 签名中。


Exercise: is for [★★★] 练习:用于[★★★]

Write a function is_for : string CharMap.t -> string CharMap.t that given an input map with bindings from k1 to v1, …, kn to vn, produces an output map with the same keys, but where each key ki is now bound to the string “ki is for vi”. For example, if m maps 'a' to "apple", then is_for m would map 'a' to "a is for apple". Hint: there is a one-line solution that uses a function from the Map.S signature. To convert a character to a string, you could use String.make. An even fancier way would be to use Printf.sprintf.
编写一个函数 is_for : string CharMap.t -> string CharMap.t ,它给出一个输入映射,该映射具有从 k1v1 、...、 knvn ,生成具有相同键的输出映射,但每个键 ki 现在绑定到字符串“ ki is for vi ”。例如,如果 m'a' 映射到 "apple" ,则 is_for m 会将 'a' 映射到 "a is for apple" 签名中的函数的单行解决方案。要将字符转换为字符串,您可以使用 String.make 。一种更奇特的方法是使用 Printf.sprintf


Exercise: first after [★★★]
练习:先后[★★★]

Write a function first_after : calendar -> Date.t -> string that returns the name of the first event that occurs strictly after the given date. If there is no such event, the function should raise Not_found, which is an exception already defined in the standard library. Hint: there is a one-line solution that uses two functions from the Map.S signature.
编写一个函数 first_after : calendar -> Date.t -> string ,返回严格在给定日期之后发生的第一个事件的名称。如果没有此类事件,该函数应引发 Not_found ,这是标准库中已定义的异常。提示:有一个单行解决方案,它使用 Map.S 签名中的两个函数。


Exercise: sets [★★★] 练习:组[★★★]

The standard library Set module is quite similar to the Map module. Use it to create a module that represents sets of case-insensitive strings. Strings that differ only in their case should be considered equal by the set. For example, the sets {“grr”, “argh”} and {“aRgh”, “GRR”} should be considered the same, and adding “gRr” to either set should not change the set.
标准库 Set 模块与 Map 模块非常相似。使用它创建一个表示不区分大小写字符串集的模块。仅大小写不同的字符串应被视为相等。例如,集合{“grr”,“argh”}和{“aRgh”,“GRR”}应被视为相同,并且将“gRr”添加到任一集合中不应更改该集合。


Exercise: ToString [★★] 练习:变字符串[★★]

Write a module type ToString that specifies a signature with an abstract type t and a function to_string : t -> string.
编写一个模块类型 ToString ,它指定具有抽象类型 t 和函数 to_string : t -> string 的签名。


Exercise: Print [★★] 练习:打印[★★]

Write a functor Print that takes as input a module named M of type ToString. The module returned by your functor should have exactly one value in it, print, which is a function that takes a value of type M.t and prints a string representation of that value.
编写一个函子 Print ,它将名为 M 且类型为 ToString 的模块作为输入。仿函数返回的模块中应该只有一个值 print ,它是一个采用 M.t 类型的值并打印该值的字符串表示形式的函数。


Exercise: Print Int [★★] 练习:打印 Int [★★]

Create a module named PrintInt that is the result of applying the functor Print to a new module Int. You will need to write Int yourself. The type Int.t should be int. Hint: do not seal Int.
创建一个名为 PrintInt 的模块,它是将函子 Print 应用于新模块 Int 的结果。您需要自己编写 Int 。类型 Int.t 应该是 int 。提示:不要密封 Int

Experiment with PrintInt in utop. Use it to print the value of an integer.
在 utop 中尝试 PrintInt 。用它来打印整数的值。


Exercise: Print String [★★]
练习:打印字符串[★★]

Create a module named PrintString that is the result of applying the functor Print to a new module MyString. You will need to write MyString yourself. Hint: do not seal MyString.
创建一个名为 PrintString 的模块,它是将函子 Print 应用于新模块 MyString 的结果。您需要自己编写 MyString 。提示:不要密封 MyString

Experiment with PrintString in utop. Use it to print the value of a string.
在 utop 中尝试 PrintString 。用它来打印字符串的值。


Exercise: Print Reuse [★]
练习:打印重用[★]

Explain in your own words how Print has achieved code reuse, albeit a very small amount.
用你自己的话解释 Print 如何实现代码重用,尽管数量很小。


Exercise: Print String reuse revisited [★★]
练习:重新审视打印字符串重用[★★]

The PrintString module you created above supports just one operation: print. It would be great to have a module that supports all the String module functions in addition to that print operation, and it would be super great to derive such a module without having to copy any code.
您上面创建的 PrintString 模块仅支持一种操作: print 。如果有一个模块除了 print 操作之外还支持所有 String 模块功能,那就太好了,并且无需复制任何内容就可以派生出这样的模块,这将是非常好的事情。代码。

Define a module StringWithPrint. It should have all the values of the built-in String module. It should also have the print operation, which should be derived from the Print functor rather than being copied code. Hint: use two include statements.
定义一个模块 StringWithPrint 。它应该具有内置 String 模块的所有值。它还应该具有 print 操作,该操作应该从 Print 函子派生,而不是复制代码。提示:使用两个 include 语句。


Exercise: implementation without interface [★]
练习:无接口实现[★]

Create a file named date.ml. In it put the following code:
创建一个名为 date.ml 的文件。在其中放入以下代码:

type date = {month : int; day : int}
let make_date month day = {month; day}
let get_month d = d.month
let get_day d = d.day
let to_string d = (string_of_int d.month) ^ "/" ^ (string_of_int d.day)

Also create a dune file:
还创建一个沙丘文件:

(library
 (name date))

Load the library into utop:
将库加载到 utop 中:

$ dune utop

In utop, open Date, create a date, access its day, and convert it to a string.
在 utop 中,打开 Date ,创建一个日期,访问其日期,并将其转换为字符串。


Exercise: implementation with interface [★]
练习:使用接口实现[★]

After doing the previous exercise, also create a file named date.mli. In it put the following code:
完成前面的练习后,还创建一个名为 date.mli 的文件。在其中放入以下代码:

type date = {month : int; day : int}
val make_date : int -> int -> date
val get_month : date -> int
val get_day : date -> int
val to_string : date -> string

Then re-do the same work as before in utop.
然后在utop中重新做之前同样的工作。


Exercise: implementation with abstracted interface [★]
练习:使用抽象接口实现[★]

After doing the previous two exercises, edit date.mli and change the first declaration in it to the following:
完成前两个练习后,编辑 date.mli 并将其中的第一个声明更改为以下内容:

type date

The type date is now abstract. Again re-do the same work in utop. Some of the responses will change. Explain in your own words those changes.
类型 date 现在是抽象的。再次在 utop 中重新做同样的工作。一些回应将会改变。用你自己的话解释这些变化。


Exercise: printer for date [★★★]
练习:日期的打印[★★★]

Add a declaration to date.mli:
date.mli 添加声明:

val format : Format.formatter -> date -> unit

And add a definition of format to date.ml. Hint: use Format.fprintf and Date.to_string.
并将 format 的定义添加到 date.ml 。提示:使用 Format.fprintfDate.to_string

Now recompile, load utop, and after loading date.cmo install the printer by issuing the directive
现在重新编译,加载 utop,加载 date.cmo 后通过发出指令安装打印机

#install_printer Date.format;;

Reissue the other phrases to utop as you did in the exercises above. The response from one phrase will change in a helpful way. Explain why.
正如您在上面的练习中所做的那样,将其他短语重新发出到 utop。一个短语的反应将会以一种有益的方式改变。解释为什么。


Exercise: refactor arith [★★★★]
练习:重构算术[★★★★]

Download this file: algebra.ml. It contains two signatures and four structures:
下载此文件:algebra.ml。它包含两个签名和四个结构:

  • Ring is signature that describes the algebraic structure called a ring, which is an abstraction of the addition and multiplication operators.
    Ring 是描述称为环的代数结构的签名,它是加法和乘法运算符的抽象。

  • Field is a signature that describes the algebraic structure called a field, which is like a ring but also has an abstraction of the division operation.
    Field 是一个描述称为域的代数结构的签名,它类似于一个环,但也具有除法运算的抽象。

  • IntRing and FloatRing are structures that implement rings in terms of int and float.
    IntRingFloatRing 是根据 intfloat 实现环的结构。

  • IntField and FloatField are structures that implement fields in terms of int and float.
    IntFieldFloatField 是根据 intfloat 实现字段的结构。

  • IntRational and FloatRational are structures that implement fields in terms of ratios (aka fractions)—that is, pairs of int and pairs of float
    IntRationalFloatRational 是按照比率(也称为分数)实现字段的结构,即成对的 int 和成对的 float
    .

Note 笔记

Dear fans of abstract algebra: of course these representations don’t necessarily obey all the axioms of rings and fields because of the limitations of machine arithmetic. Also, the division operation in IntField is ill-defined on zero. Try not to worry about that.
亲爱的抽象代数爱好者:当然,由于机器算术的限制,这些表示不一定遵守环和域的所有公理。此外, IntField 中的除法运算对于零的定义不明确。尽量不要担心这个。

Refactor the code to improve the amount of code reuse it exhibits. To do that, use include, functors, and introduce additional structures and signatures as needed. There isn’t necessarily a right answer here, but here’s some advice:
重构代码以提高代码重用率。为此,请使用 include 、函子,并根据需要引入其他结构和签名。这里不一定有正确的答案,但这里有一些建议:

  • No name should be directly declared in more than one signature. For example, ( + ) should not be directly declared in Field; it should be reused from an earlier signature. By “directly declared” we mean a declaration of the form val name : .... An indirect declaration would be one that results from an include.
    不得在多个签名中直接声明姓名。例如, ( + ) 不应直接在 Field 中声明;应该从较早的签名中重新使用它。 “直接声明”是指 val name : ... 形式的声明。间接声明是由 include 产生的声明。

  • You need only three direct definitions of the algebraic operations and numbers (plus, minus, times, divide, zero, one): once for int, once for float, and once for ratios. For example, IntField.( + ) should not be directly defined as Stdlib.( + ); rather, it should be reused from elsewhere. By “directly defined” we mean a definition of the form let name = .... An indirect definition would be one that results from an include or a functor application.
    您只需要代数运算和数字的三个直接定义(加、减、乘、除、零、一):一次用于 int ,一次用于 float ,一次用于比率。例如, IntField.( + ) 不应直接定义为 Stdlib.( + ) ;相反,它应该从其他地方重用。 “直接定义”是指 let name = ... 形式的定义。间接定义是由 include 或函子应用程序产生的定义。

  • The rational structures can both be produced by a single functor that is applied once to IntField and once to FloatField.
    有理结构都可以由单个函子产生,该函子分别应用于 IntFieldFloatField

  • It’s possible to eliminate all duplication of of_int, such that it is directly defined exactly once, and all structures reuse that definition; and such that it is directly declared in only one signature. This will require the use of functors. It will also require inventing an algorithm that can convert an integer to an arbitrary Ring representation, regardless of what the representation type of that Ring is.
    可以消除 of_int 的所有重复,这样它就直接定义一次,并且所有结构都重用该定义;并且仅在一个签名中直接声明它。这将需要使用函子。它还需要发明一种算法,可以将整数转换为任意 Ring 表示形式,无论该 Ring 表示形式是什么。

When you’re done, the types of all the modules should remain unchanged. You can easily see those types by running ocamlc -i algebra.ml.
完成后,所有模块的类型应保持不变。您可以通过运行 ocamlc -i algebra.ml 轻松查看这些类型。

6. Correctness 6. 正确性 ¶

When we write code, we always hope that we get it right. We hope that our code is correct. But how can we know it’s correct? In this chapter, we’ll study three possible answers: documentation, testing, and proof.
当我们编写代码时,我们总是希望自己能写对。我们希望我们的代码是正确的。但我们怎么知道它是正确的呢?在本章中,我们将研究三种可能的答案:文档、测试和证明。

Let’s be honest: we all at one time or another have thought that documentation or testing was a boring, tedious, and altogether postponable task. But with maturity programmers come to realize that both are essential to writing correct code. Both get at the truth of what code really does.
说实话:我们都曾一度认为文档或测试是一项无聊、乏味且完全可以推迟的任务。但随着成熟,程序员开始意识到两者对于编写正确的代码都是必不可少的。两者都了解了代码真正的作用。

Documentation is the ground truth of what a programmer intended, as opposed to what they actually wrote. It communicates to other humans the ideas the author had in their head. No small amount of the time (even in this book!), we fail at communicating ideas as we intended. Maybe the failure occurs in the code, or maybe in the documentation. But writing documentation forces us to think a second (er, hopefully second) time about our intentions. The cognitive task of explaining our ideas to other humans is certainly different than explaining our ideas to the computer. That can expose failures in our thinking.
文档 是程序员意图的基本事实,而不是他们实际编写的内容。它将作者头脑中的想法传达给其他人。很多时候(即使是在本书中!),我们也未能按照我们的预期传达想法。失败可能发生在代码中,也可能发生在文档中。但是编写文档迫使我们第二次(呃,希望是第二次)思考我们的意图。向其他人解释我们的想法的认知任务肯定不同于向计算机解释我们的想法。这可以暴露我们思维中的失败。

More importantly, documentation is a message in a time capsule. Imagine this: someone far away and now unreachable has sent that message to you, the programmer. You need that message to interpret the archeological evidence now in front of you—i.e., the otherwise unintelligible source code you have inherited. Your only hope is that the original author, long ago, had enough empathy to commit their thoughts to the written word.
更重要的是,文档是时间胶囊中的消息。想象一下:某个遥远且现在无法联系的人已将该消息发送给您(程序员)。您需要该消息来解释现在摆在您面前的考古证据,即您继承的难以理解的源代码。你唯一的希望是原作者很久以前就有足够的同理心将他们的想法写成文字。

And now imagine this: that author from the distant past? What if they were YOU? It might be you from two weeks ago, two months ago, or two years ago. Human memory is fleeting. If you’ve only been programming for a couple of years yourself, this can be difficult to understand, but give it a generous try: Someday, you’re going to come back to the code you’re writing today and have no clue what it means. Your only hope is to leave yourself some breadcrumbs at the time you write it. Otherwise, you’ll be lost when you circle back.
现在想象一下:那个来自遥远过去的作家?如果他们是你怎么办?可能是两周前、两个月前或两年前的你。人类的记忆是转瞬即逝的。如果您自己只编程了几年,这可能很难理解,但请大胆尝试:有一天,您将回到今天编写的代码,并且不知道什么它的意思是。你唯一的希望就是在你写它的时候给自己留下一些面包屑。否则,当你绕回来时,你就会迷路。

Testing is the ground truth of what a program actually does, as opposed to what the programmer intended. It provides evidence that the programmer got it right. Good scientists demand evidence. That demand comes not out of arrogance but humility. We human beings are so amazingly good at deluding ourselves. (Consider the echo chamber of modern social media.) You can write a piece of code that you think is right. But then you can write a test case that demonstrates it’s right. Then you can write ten more. The evidence accumulates, and eventually it’s enough to be convincing. Is it absolute? Of course not. Maybe there’s some test case you weren’t clever enough to invent. That’s science: new ideas come along to challenge the old.
测试 是程序实际执行的操作的基本事实,而不是程序员的意图。它提供了程序员做对了的证据。优秀的科学家需要证据。这种要求并非出于傲慢,而是出于谦卑。我们人类非常擅长欺骗自己。 (考虑现代社交媒体的回声室。)您可以编写一段您认为正确的代码。但随后您可以编写一个测试用例来证明它是正确的。然后你可以再写十个。证据不断积累,最终就足以令人信服。是绝对的吗?当然不是。也许有些测试用例是你不够聪明而无法发明的。这就是科学:新想法的出现挑战旧想法。

Even more importantly, testing is repeatable science. The ability to replicate experiments is crucial to the truth they establish. By capturing tests as automatically repeatable experiments as unit test suites, we can demonstrate to ourselves and other, now and in the future, that our code is correct.
更重要的是,测试是可重复的科学。复制实验的能力对于他们建立的真理至关重要。通过将测试捕获为可自动重复的实验(如单元测试套件),我们可以向自己和其他人证明,现在和将来,我们的代码是正确的。

The challenge of documentation and testing is discipline. It’s so tempting, so easy, to care only about writing the code. “That’s the fun part”, right? But it’s like leaving out a third of the letter we intended to write. One part of the letter is to the machine, regarding how to compute. But another part is to other humans, about what we wanted to compute. And another part is to both machines and humans, about what we really did manage to compute. Your job isn’t done until all three parts have been written.
文档和测试的 挑战 是纪律。只关心编写代码是如此诱人,如此简单。 “这就是有趣的部分”,对吧?但这就像遗漏了我们打算写的信的三分之一。这封信的一部分是写给机器的,关于如何计算。但另一部分是关于其他人的,关于我们想要计算的内容。另一部分是关于机器和人类的,关于我们真正设法计算的内容。在所有三个部分都写完之前,你的工作才算完成。

If you’re not yet convinced about the importance of documentation and testing, no worries. You will be in the future, if you stick with the craft of programming long enough. Meanwhile, let’s proceed with learning about how to do it better. In this chapter, we’re going to learn about some successful (and hopefully new-to-you) techniques for both.
如果您还不相信文档和测试的重要性,不用担心。如果你坚持编程技巧足够长的时间,你就会在未来。同时,让我们继续学习如何做得更好。在本章中,我们将学习一些成功的(希望对您来说是新的)技术。

Finally, beyond documentation and testing, there is mathematical proof of correctness. Techniques from logic and discrete math can be used to formally prove that a program is correct according to a specification. Such proofs aren’t necessarily easy—in fact they take even more human discipline and training than documentation and testing do. But they can make sense to apply when programs are used for safety critical tasks where human lives are on the line.
最后,除了文档和测试之外,还有正确性的数学 证明 。逻辑和离散数学技术可用于正式证明程序根据规范是正确的。这样的证明并不一定容易——事实上,它们比文档和测试需要更多的人类纪律和培训。但当程序用于危及人命的安全关键任务时,它们的应用就有意义了。

6.1. Specifications 6.1. 规范 ¶

A specification is a contract between a client of some unit of code and the implementer of that code. The most common place we find specifications is as comments in the interface (.mli) files for a module. There, the implementer of the module spells out what the client may and may not assume about the module’s behavior. This contract makes it clear who to blame if something goes wrong: Did the client misuse the module? Or did the implementer fail to deliver the promised functionality?
规范是某个代码单元的客户端和该代码的实现者之间的契约。我们发现规范最常见的地方是模块的接口 ( .mli ) 文件中的注释。在那里,模块的实现者详细说明了客户端可以假设和不可以假设模块的行为。该合同明确了如果出现问题应归咎于谁:客户是否滥用了该模块?或者实施者未能提供承诺的功能?

Specifications usually involve preconditions and postconditions. The preconditions inform what the client must guarantee about inputs they pass in, and what the implementer may assume about those inputs. The postconditions inform what they client may assume about outputs they receive, and what the implementer must guarantee about those outputs.
规范通常涉及前置条件和后置条件。前提条件告知客户端必须保证他们传入的输入什么,以及实现者可以对这些输入假设什么。后置条件告知客户可以对他们收到的输出做出什么假设,以及实施者必须对这些输出做出什么保证。

An implementation satisfies a specification if it provides the behavior described by the specification. There may be many possible implementations of a given specification that are feasible. The client may not assume anything about which of those implementations is actually provided. The implementer, on the other hand, gets to provide one of their choice.
如果一个实现提供了规范所描述的行为,则它满足规范。给定规范可能有许多可行的实现方式。客户端可能不会假设实际提供了哪些实现。另一方面,实施者可以提供他们的选择。

Clear specifications serve many important functions in software development teams. One important one is when something goes wrong, everyone can agree on whose job it is to fix the problem: either the implementer has not met the specification and needs to fix the implementation, or the client has written code that assumes something not guaranteed by the spec, and therefore needs to fix the using code. Or, perhaps the spec is wrong, and then the client and implementer need to decide on a new spec. This ability to decide whose problem a bug is prevents problems from slipping through the cracks.
清晰的规范在软件开发团队中发挥着许多重要作用。一个重要的一点是,当出现问题时,每个人都可以就解决问题的工作达成一致:要么实现者没有满足规范并需要修复实现,要么客户编写的代码假设了某些无法由客户保证的事情。规范,因此需要修复使用代码。或者,也许规范是错误的,然后客户和实施者需要决定新的规范。这种确定 bug 是谁的问题的能力可以防止问题被忽视。

Writing Specifications. Good specifications have to balance two conflicting goals; they must be
编写规范。好的规范必须平衡两个相互冲突的目标;他们一定是

  • sufficiently restrictive, ruling out implementations that would be useless to clients, as well as
    足够的限制,排除对客户无用的实现,以及

  • sufficiently general, not ruling out implementations that would be useful to clients.
    足够通用,不排除对客户有用的实现。

Some common mistakes include not stating enough in preconditions, failing to identify when exceptions will be thrown, failing to specify behavior at boundary cases, writing operational specifications instead of definitional and stating too much in postconditions.
一些常见的错误包括在前置条件中声明得不够,未能确定何时抛出异常,未能指定边界情况下的行为,编写操作规范而不是定义性规范以及在后置条件中声明过多。

Writing good specifications is hard because the language and compiler do nothing to check the correctness of a specification: there’s no type system for them, no warnings, etc. (Though there is ongoing research on how to improve specifications and the writing of them.) The specifications you write will be read by other people, and with that reading can come misunderstanding. Reading specifications requires close attention to detail.
编写好的规范很困难,因为语言和编译器不检查规范的正确性:没有类型系统,没有警告等。(尽管正在研究如何改进规范及其编写。)您编写的规范将被其他人阅读,而阅读可能会带来误解。阅读规范需要密切关注细节。

Specifications should be written quite early. As soon as a design decision is made, document it in a specification. Specifications should continue to be updated throughout implementation. A specification becomes obsolete only when the code it specifies becomes obsolete and is removed from the code base.
规范应该尽早编写。一旦做出设计决策,就将其记录在规范中。规范应在整个实施过程中不断更新。仅当规范指定的代码已过时并从代码库中删除时,该规范才会过时。

Abstraction by Specification. Abstraction enables modular programming by hiding the details of implementations. Specifications are a part of that kind of abstraction: they reveal certain information about the behavior of a module without disclosing all the details of the module’s implementation.
按规范抽象。抽象通过隐藏实现的细节来实现模块化编程。规范是这种抽象的一部分:它们揭示了有关模块行为的某些信息,但不披露模块实现的所有细节。

Locality is one of the benefits of abstraction by specification. A module can be understood without needing to examine its implementation. This locality is critical in implementing large programs, and even in implementing smaller programs in teams. No one person can keep the entire system in their head at a time.
局部性是通过规范进行抽象的好处之一。无需检查其实现即可理解模块。这个位置对于实施大型项目甚至在团队中实施较小的项目都至关重要。没有人能够同时将整个系统牢记于心。

Modifiability is another benefit. Modules can be reimplemented without changing the implementation of other modules or functions. Software libraries depend upon this to improve their functionality without forcing all their clients to rewrite code every time the library is upgraded. Modifiability also enables performance enhancements: we can write simple, slow implementations first, then improve bottlenecks as necessary.
可修改性是另一个好处。可以重新实现模块,而无需更改其他模块或功能的实现。软件库依赖于此来改进其功能,而无需在每次升级库时强迫所有客户端重写代码。可修改性还可以增强性能:我们可以首先编写简单、缓慢的实现,然后根据需要改进瓶颈。

The client should not assume more about the implementation than is given in the spec because that allows the implementation to change. The specification forms an abstraction barrier that protects the implementer from the client and vice versa. Making assumptions about the implementation that are not guaranteed by the specification is known as violating the abstraction barrier. The abstraction barrier enforces local reasoning. Further, it promotes loose coupling between different code modules. If one module changes, other modules are less likely to have to change to match.
客户端不应假设比规范中给出的更多的实现,因为这允许实现更改。该规范形成了一个抽象屏障,保护实现者免受客户端的影响,反之亦然。对规范无法保证的实现做出假设被称为违反抽象障碍。抽象障碍强化了局部推理。此外,它促进了不同代码模块之间的松散耦合。如果一个模块发生变化,其他模块不太可能需要进行更改以匹配。

6.2. Function Documentation
6.2. 函数文档 ¶

This section continues the discussion of documentation, which we began in chapter 2.
本节继续我们在第 2 章中开始的文档讨论。

A specification is written for humans to read, not machines. “Specs” can take time to write well, and it is time well spent. The main goal is clarity. It is also important to be concise, because client programmers will not always take the effort to read a long spec. As with anything we write, we need to be aware of our audience when writing specifications. Some readers may need a more verbose specification than others.
规范是为人类阅读而编写的,而不是为机器阅读的。写好“规格”可能需要时间,但花时间是值得的。主要目标是清晰度。简洁也很重要,因为客户端程序员不会总是花精力去阅读很长的规范。与我们编写的任何内容一样,我们在编写规范时需要了解我们的受众。有些读者可能需要比其他读者更详细的规范。

A well-written specification usually has several parts communicating different kinds of information about the thing specified. If we know what the usual ingredients of a specification are, we are less likely to forget to write down something important. Let’s now look at a recipe for writing specifications.
编写良好的规范通常有几个部分来传达有关指定事物的不同类型的信息。如果我们知道规范的通常组成部分是什么,我们就不太可能忘记写下重要的东西。现在让我们看一下编写规范的秘诀。

6.2.1. Returns Clause 6.2.1. 返回子句 ¶

How might we specify sqr, a square-root function? First, we need to describe its result. We will call this description the returns clause because it is a part of the specification that describes the result of a function call. It is also known as a postcondition: it describes a condition that holds after the function is called. Here is an example of a returns clause:
我们如何指定 sqr (平方根函数)?首先,我们需要描述它的结果。我们将此描述称为 returns 子句,因为它是描述函数调用结果的规范的一部分。它也称为后置条件:它描述了调用函数后成立的条件。以下是 returns 子句的示例:

(** returns: [sqr x] is the square root of [x]. *)

But we would typically leave out the returns:, and simply write the returns clause as the first sentence of the comment:
但我们通常会省略 returns: ,并简单地将 returns 子句写为注释的第一句:

(** [sqr x] is the square root of [x]. *)

For numerical programming, we should probably add some information about how accurate it is.
对于数值编程,我们可能应该添加一些有关其准确性的信息。

(** [sqr x] is the square root of [x]. Its relative accuracy is no worse than
    [1.0e-6]. *)

Similarly, here’s how we might write a returns clause for a find function:
类似地,我们可以这样为 find 函数编写 returns 子句:

(** [find lst x] is the index of [x] in [lst], starting from zero. *)

A good specification is concise but clear—it should say enough that the reader understands what the function does, but without extra verbiage to plow through and possibly cause the reader to miss the point. Sometimes there is a balance to be struck between brevity and clarity.
一个好的规范是简洁而清晰的——它应该足以让读者理解该函数的作用,但没有多余的措辞来深入理解,并可能导致读者错过要点。有时需要在简洁性和清晰度之间取得平衡。

These two specifications use a useful trick to make them more concise: they talk about the result of applying the function being specified to some arbitrary arguments. Implicitly we understand that the stated postcondition holds for all possible values of any unbound variables (the argument variables).
这两个规范使用了一个有用的技巧来使它们更加简洁:它们讨论将指定的函数应用于某些任意参数的结果。我们隐含地理解,所声明的后置条件适用于任何未绑定变量(参数变量)的所有可能值。

6.2.2. Requires Clause 6.2.2. 前提子句 ¶

The specification for sqr doesn’t completely make sense because the square root does not exist for some x of type real. The mathematical square root function is a partial function that is defined over only part of its domain. A good function specification is complete with respect to the possible inputs; it provides the client with an understanding of what inputs are allowed and what the results will be for allowed inputs.
sqr 的规范并不完全有意义,因为对于 real 类型的某些 x 来说,平方根不存在。数学平方根函数是仅在其域的一部分上定义的偏函数。良好的功能规范对于可能的输入来说是完整的;它使客户了解允许哪些输入以及允许输入的结果是什么。

We have several ways to deal with partial functions. A straightforward approach is to restrict the domain so that it is clear the function cannot be legitimately used on some inputs. The specification rules out bad inputs with a requires clause establishing when the function may be called. This clause is also called a precondition because it describes a condition that must hold before the function is called. Here is a requires clause for sqr:
我们有几种方法来处理部分函数。一种简单的方法是限制域,以便清楚地表明该函数不能在某些输入上合法使用。该规范通过确定何时可以调用该函数的 require 子句来排除错误的输入。该子句也称为前置条件,因为它描述了调用函数之前必须满足的条件。这是 sqr 的 require 子句:

(** [sqr x] is the square root of [x]. Its relative accuracy is no worse
    than [1.0e-6].  Requires: [x >= 0] *)

This specification doesn’t say what happens when x < 0, nor does it have to. Remember that the specification is a contract. This contract happens to push the burden of showing that the square root exists onto the client. If the requires clause is not satisfied, the implementation is permitted to do anything it likes: for example, go into an infinite loop or throw an exception. The advantage of this approach is that the implementer is free to design an algorithm without the constraint of having to check for invalid input parameters, which can be tedious and slow down the program. The disadvantage is that it may be difficult to debug if the function is called improperly, because the function can misbehave and the client has no understanding of how it might misbehave.
该规范没有说明 x < 0 时会发生什么,也没有必要说明。请记住,该规范是一份合同。该合同恰好将证明平方根存在的负担推给了客户。如果不满足requires子句,则允许实现执行任何它喜欢的操作:例如,进入无限循环或抛出异常。这种方法的优点是实现者可以自由地设计算法,而不必检查无效的输入参数,这可能是乏味的并且会减慢程序速度。缺点是,如果函数调用不正确,调试可能会很困难,因为函数可能会出现错误行为,而客户端不知道它可能会如何出现错误行为。

6.2.3. Raises Clause 6.2.3. 加注子句 ¶

Another way to deal with partial functions is to convert them into total functions (functions defined over their entire domain). This approach is arguably easier for the client to deal with because the function’s behavior is always defined; it has no precondition. However, it pushes work onto the implementer and may lead to a slower implementation.
处理部分函数的另一种方法是将它们转换为全函数(在整个域上定义的函数)。这种方法对于客户来说可以说更容易处理,因为函数的行为总是被定义的;它没有先决条件。然而,它将工作推给了实施者,并可能导致实施速度变慢。

How can we convert sqr into a total function? One approach that is (too) often followed is to define some value that is returned in the cases that the requires clause would have ruled; for example:
我们如何将 sqr 转换为total函数?一种(太)经常遵循的方法是定义在需要子句规则的情况下返回的一些值;例如:

(** [sqr x] is the square root of [x] if [x >= 0],
    with relative accuracy no worse than 1.0e-6.
    Otherwise, a negative number is returned. *)

This practice is not recommended because it tends to encourage broken, hard-to-read client code. Almost any correct client of this abstraction will write code like this if the precondition cannot be argued to hold:
不推荐这种做法,因为它往往会导致客户代码损坏、难以阅读。如果不能证明前提条件成立,则几乎任何正确的此抽象客户都会编写如下代码:

if sqr(a) < 0.0 then ... else ...

The error must still be handled in the if expression, so the job of the client of this abstraction isn’t any easier than with a requires clause: the client still needs to wrap an explicit test around the call in cases where it might fail. If the test is omitted, the compiler won’t complain, and the negative number result will be silently treated as if it were a valid square root, likely causing errors later during program execution. This coding style has been the source of innumerable bugs and security problems in the Unix operating systems and its descendents (e.g., Linux).
错误仍然必须在 if 表达式中处理,因此这个抽象的客户端的工作并不比使用 require 子句更容易:客户端仍然需要围绕调用进行显式测试可能会失败的情况。如果省略测试,编译器不会抱怨,并且负数结果将被默默地视为有效的平方根,可能会在程序执行期间导致错误。这种编码风格是 Unix 操作系统及其后代(例如 Linux)中无数错误和安全问题的根源。

A better way to make functions total is to have them raise an exception when the expected input condition is not met. Exceptions avoid the necessity of distracting error-handling logic in the client’s code. If the function is to be total, the specification must say what exception is raised and when. For example, we might make our square root function total as follows:
使函数完整的更好方法是让它们在不满足预期输入条件时引发异常。异常避免了分散客户端代码中的错误处理逻辑的必要性。如果函数是完整的,则规范必须说明引发什么异常以及何时引发。例如,我们可以将平方根函数总计如下:

(** [sqr x] is the square root of [x], with relative accuracy no worse
    than 1.0e-6. Raises: [Negative] if [x < 0]. *)

Note that the implementation of this sqr function must check whether x >= 0, even in the production version of the code, because some client may be relying on the exception to be raised.
请注意,此 sqr 函数的实现必须检查是否 x >= 0 ,即使在代码的生产版本中也是如此,因为某些客户端可能依赖于引发异常。

6.2.4. Examples Clause 6.2.4. 示例子句 ¶

It can be useful to provide an illustrative example as part of a specification. No matter how clear and well written the specification is, an example is often useful to clients.
提供说明性示例作为规范的一部分可能很有用。无论规范写得多清晰、写得多好,示例通常对客户来说都是有用的。

(** [find lst x] is the index of [x] in [lst], starting from zero.
    Example: [find ["b","a","c"] "a" = 1]. *)

6.2.5. The Specification Game
6.2.5. 关于规范的游戏 ¶

When evaluating specifications, it can be useful to imagine that a game is being played between two people: a specifier and a devious programmer.
在评估规范时,想象一下游戏是在两个人之间进行的:规范制定者和狡猾的程序员。

Suppose that the specifier writes the following specification:
假设说明符编写了以下说明:

(** returns a list *)
val reverse : 'a list -> 'a list

This spec is clearly incomplete. For example, a devious programmer could meet the spec with an implementation that gives the following output:
该规范显然是不完整的。例如,狡猾的程序员可以通过提供以下输出的实现来满足规范:

# reverse [1; 2; 3];;
- : int list = []

The specifier, upon realizing this, refines the spec as follows:
规范制定者意识到这一点后,将规范细化如下:

(** [reverse lst] returns a list that is the same length as [lst] *)
val reverse : 'a list -> 'a list

But the specifier discovers that the spec still allows broken implementations:
但规范者发现规范仍然允许损坏的实现:

# reverse [1; 2; 3];;
- : int list = [0; 0; 0]

Finally, the specifier settles on a third spec:
最后,规范制定者确定了第三个规范:

(** [reverse lst] returns a list [m] satisfying the following conditions:
    - [length lst = length m]
    - for all [i], [nth m i = nth lst (n - i - 1)],
      where [n] is the length of [lst].
    For example, [reverse [1; 2; 3]] is [3; 2; 1], and [reverse []] is []. *)
val reverse : 'a list -> 'a list

With this spec, the devious programmer is forced to provide a working implementation to meet the spec, so the specifier has successfully written her spec.
有了这个规范,狡猾的程序员就被迫提供一个工作实现来满足规范,因此规范制定者已经成功地编写了她的规范。

The point of playing this game is to improve your ability to write specifications. Obviously we’re not advocating that you deliberately try to violate the intent of a specification and get away with it. When reading someone else’s specification, read as generously as possible. But be ruthless about improving your own specifications.
玩这个游戏的目的是提高你编写规范的能力。显然,我们并不提倡您故意违反规范的意图并侥幸逃脱惩罚。当阅读别人的规范时,请尽可能慷慨地阅读。但要毫不留情地提高自己的规格。

6.2.6. Comments 6.2.6. 注释 ¶

In addition to specifying functions, programmers need to provide comments in the body of the functions. In fact, programmers usually do not write enough comments in their code. (For a classic example, check out the actual comment on line 561 of the Quake 3 Arena game engine.)
除了指定函数之外,程序员还需要在函数体中提供注释。事实上,程序员通常不会在代码中编写足够的注释。 (有关经典示例,请查看 Quake 3 Arena 游戏引擎第 561 行的实际注释。)

But this doesn’t mean that adding more comments is always better. The wrong comments will simply obscure the code further. Shoveling as many comments into code as possible usually makes the code worse! Both code and comments are precise tools for communication (with the computer and with other programmers) that should be wielded carefully.
但这并不意味着添加更多注释总是更好。错误的注释只会进一步模糊代码。将尽可能多的注释放入代码中通常会使代码变得更糟!代码和注释都是(与计算机和其他程序员)交流的精确工具,应谨慎使用。

It is particularly annoying to read code that contains many interspersed comments (typically of questionable value), e.g.:
阅读包含许多散布注释(通常具有可疑价值)的代码尤其令人烦恼,例如:

let y = x + 1 (* make y one greater than x *)

For complex algorithms, some comments may be necessary to explain how the code implementing the algorithm works. Programmers are often tempted to write comments about the algorithm interspersed through the code. But someone reading the code will often find these comments confusing because they don’t have a high-level picture of the algorithm. It is usually better to write a paragraph-style comment at the beginning of the function explaining how its implementation works. Explicit points in the code that need to be related to that paragraph can then be marked with very brief comments, like (* case 1 *).
对于复杂的算法,可能需要一些注释来解释实现算法的代码如何工作。程序员常常想在代码中写下关于算法的注释。但是阅读代码的人通常会发现这些注释令人困惑,因为他们没有对算法的高级了解。通常最好在函数的开头编写一段段落式注释来解释其实现的工作原理。然后可以用非常简短的注释来标记代码中需要与该段落相关的显式点,例如 (* case 1 *)

Another common but well-intentioned mistake is giving variables long, descriptive names, as in the following verbose code:
另一个常见但善意的错误是为变量提供长的描述性名称,如以下详细代码所示:

let number_of_zeros the_list =
  List.fold_left (fun (accumulator : int) (list_element : int) ->
    accumulator + (if list_element = 0 then 1 else 0)) 0 the_list
val number_of_zeros : int list -> int = <fun>

Code using such long names is verbose and hard to read. Instead of trying to embed a complete description of a variable in its name, use a short and suggestive name (e.g., zeros), and if necessary, add a comment at its declaration explaining the purpose of the variable.
使用如此长的名称的代码非常冗长且难以阅读。不要尝试在变量的名称中嵌入变量的完整描述,而应使用简短且具有暗示性的名称(例如 zeros ),并在必要时在其声明中添加注释来解释变量的用途。

let zeros lst =
  let is0 = function 0 -> 1 | _ -> 0 in
  List.fold_left (fun zs x -> zs + is0 x) 0 lst
val zeros : int list -> int = <fun>

A similarly bad practice is to encode the type of the variable in its name, e.g. naming a variable i_count to show that it’s an integer. The type system is going to guarantee that for you, and your editor can provide a hover-over to show the type. If you really want to emphasize the type in the code, add a type annotation at the point where the variable comes into scope.
一个类似的糟糕做法是在变量名称中编码变量的类型,例如命名变量 i_count 以表明它是一个整数。类型系统将为您保证这一点,并且您的编辑器可以提供悬停来显示类型。如果您确实想强调代码中的类型,请在变量进入作用域的位置添加类型注释。

6.3. Module Documentation
6.3. 模块文档 ¶

The specification of functions provided by a module can be found in its interface, which is what clients will consult. But what about internal documentation, which is relevant to those who implement and maintain a module? The purpose of such implementation comments is to explain to the reader how the implementation correctly implements its interface.
模块提供的功能说明可以在其接口中找到,供客户查阅。但是与实施和维护模块的人员相关的内部文档又如何呢?此类实现注释的目的是向读者解释实现如何正确实现其接口。

Reminder 提醒

It is inappropriate to copy the specifications of functions found in the module interface into the module implementation. Copying runs the risk of introducing inconsistency as the program evolves, because programmers don’t keep the copies in sync. Copying code and specifications is a major source (if not the major source) of program bugs. In any case, implementers can always look at the interface for the specification.
将模块接口中的函数规范复制到模块实现中是不合适的。随着程序的发展,复制存在引入不一致的风险,因为程序员不会保持副本同步。复制代码和规范是程序错误的主要来源(如果不是主要来源)。无论如何,实现者总是可以查看接口的规范。

Implementation comments fall into two categories. The first category arises because a module implementation may define new types and functions that are purely internal to the module. If their significance is not obvious, these types and functions should be documented in much the same style that we have suggested for documenting interfaces. Often, as the code is written, it becomes apparent that the new types and functions defined in the module form an internal data abstraction or at least a collection of functionality that makes sense as a module in its own right. This is a signal that the internal data abstraction might be moved to a separate module and manipulated only through its operations.
实施意见分为两类。第一类的出现是因为模块实现可能定义纯粹在模块内部的新类型和函数。如果它们的重要性不明显,那么这些类型和函数应该以与我们建议的接口文档风格大致相同的风格来文档化。通常,随着代码的编写,模块中定义的新类型和函数会形成一个内部数据抽象,或者至少是一个功能集合,这些功能本身作为一个模块是有意义的。这是一个信号,表明内部数据抽象可能会移动到单独的模块并仅通过其操作进行操作。

The second category of implementation comments is associated with the use of data abstraction. Suppose we are implementing an abstraction for a set of items of type 'a. The interface might look something like this:
第二类实现注释与数据抽象的使用相关。假设我们正在实现一组 'a 类型的项目的抽象。界面可能看起来像这样:

(** A set is an unordered collection in which multiplicity is ignored. *)
module type Set = sig

  (** ['a t] represents a set whose elements are of type ['a] *)
  type 'a t

  (** [empty] is the set containing no elements *)
  val empty : 'a t

  (** [mem x s] is whether [x] is a member of set [s] *)
  val mem : 'a -> 'a t -> bool

  (** [add x s] is the set containing all the elements of [s]
      as well as [x]. *)
  val add : 'a -> 'a t -> 'a t

  (** [rem x s] is the set containing all the elements of [s],
      minus [x]. *)
  val rem : 'a -> 'a t -> 'a t

  (** [size s] is the cardinality of [s] *)
  val size: 'a t -> int

  (** [union s1 s2] is the set containing all the elements that
      are in either [s1] or [s2]. *)
  val union: 'a t -> 'a t -> 'a t

  (** [inter s1 s2] is the set containing all the elements that
      are in both [s1] and [s2]. *)
  val inter: 'a t -> 'a t -> 'a t
end
module type Set =
  sig
    type 'a t
    val empty : 'a t
    val mem : 'a -> 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val rem : 'a -> 'a t -> 'a t
    val size : 'a t -> int
    val union : 'a t -> 'a t -> 'a t
    val inter : 'a t -> 'a t -> 'a t
  end

In a real signature for sets, we’d want operations such as map and fold as well, but let’s omit these for now for simplicity. There are many ways to implement this abstraction.
在集合的真实签名中,我们还需要诸如 mapfold 之类的操作,但为了简单起见,我们现在忽略这些操作。有很多方法可以实现这种抽象。

As we’ve seen before, one easy way is as a list:
正如我们之前所见,一种简单的方法是使用列表:

(** Implementation of sets as lists with duplicates *)
module ListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add = List.cons
  let rem x = List.filter (( <> ) x)
  let size lst = List.(lst |> sort_uniq Stdlib.compare |> length)
  let union lst1 lst2 = lst1 @ lst2
  let inter lst1 lst2 = List.filter (fun h -> mem h lst2) lst1
end
module ListSet : Set

This implementation has the advantage of simplicity. For small sets that tend not to have duplicate elements, it will be a fine choice. Its performance will be poor for large sets or applications with many duplicates but for some applications that’s not an issue.
这种实现方式的优点是简单。对于往往没有重复元素的小集合,这将是一个不错的选择。对于大型集或具有许多重复项的应用程序来说,它的性能会很差,但对于某些应用程序来说,这不是问题。

Notice that the types of the functions do not need to be written down in the implementation. They aren’t needed because they’re already present in the signature, just like the specifications that are also in the signature don’t need to be replicated in the structure.
请注意,函数的类型不需要在实现中写下来。不需要它们,因为它们已经存在于签名中,就像签名中的规范不需要在结构中复制一样。

Here is another implementation of Set that also uses 'a list but requires the lists to contain no duplicates. This implementation is also correct (and also slow for large sets). Notice that we are using the same representation type, yet some important aspects of the implementation (add, size, union) are quite different.
这是 Set 的另一个实现,它也使用 'a list 但要求列表不包含重复项。这个实现也是正确的(对于大集合来说也很慢)。请注意,我们使用相同的表示类型,但实现的一些重要方面( addsizeunion )却截然不同。

(** Implementation of sets as lists without duplicates *)
module UniqListSet : Set = struct
  type 'a t = 'a list
  let empty = []
  let mem = List.mem
  let add x lst = if mem x lst then lst else x :: lst
  let rem x = List.filter (( <> ) x)
  let size = List.length
  let union lst1 lst2 = lst1 @ lst2 |> List.sort_uniq Stdlib.compare
  let inter lst1 lst2 = List.filter (fun h -> mem h lst2) lst1
end
module UniqListSet : Set

An important reason why we introduced the writing of function specifications was to enable local reasoning: once a function has a spec, we can judge whether the function does what it is supposed to without looking at the rest of the program. We can also judge whether the rest of the program works without looking at the code of the function. However, we cannot reason locally about the individual functions in the three module implementations just given. The problem is that we don’t have enough information about the relationship between the concrete type (int list) and the corresponding abstract type (set). This lack of information can be addressed by adding two new kinds of comments to the implementation: the abstraction function and the representation invariant for the abstract data type. We turn to discussion of those, next.
我们引入函数规范编写的一个重要原因是为了实现本地推理:一旦一个函数有了规范,我们就可以判断该函数是否做了它应该做的事情,而无需查看程序的其余部分。我们也可以在不看函数代码的情况下判断程序的其余部分是否工作。然而,我们无法对刚刚给出的三个模块实现中的各个函数进行本地推理。问题是我们没有足够的信息来了解具体类型 ( int list ) 和相应的抽象类型 ( set ) 之间的关系。这种信息的缺乏可以通过在实现中添加两种新的注释来解决:抽象函数和抽象数据类型的表示不变式。接下来我们将讨论这些内容。

6.3.1. Abstraction Functions
6.3.1. 抽象函数 ¶

The client of any Set implementation should not be able to distinguish it from any other implementation based on its functional behavior. As far as the client can tell, the operations act like operations on the mathematical ideal of a set. In the first implementation, the lists [3; 1], [1; 3], and [1; 1; 3] are distinguishable to the implementer, but not to the client. To the client, they all represent the abstract set {1, 3} and cannot be distinguished by any of the operations of the Set signature. From the point of view of the client, the abstract data type describes a set of abstract values and associated operations. The implementer knows that these abstract values are represented by concrete values that may contain additional information invisible from the client’s view. This loss of information is described by the abstraction function, which is a mapping from the space of concrete values to the abstract space. The abstraction function for the implementation ListSet looks like this:
任何 Set 实现的客户端不应能够根据其功能行为将其与任何其他实现区分开。据客户所知,这些操作就像对集合的数学理想进行操作。在第一个实现中,列表 [3; 1][1; 3][1; 1; 3] 对于实现者来说是可区分的,但对于用户来说是不可区分的。对于客户来说,它们都代表抽象集合{1, 3},无法通过 Set 签名的任何操作来区分。从客户的角度来看,抽象数据类型描述了一组抽象值和关联的操作。实现者知道这些抽象值由具体值表示,这些具体值可能包含从客户一端的视图中不可见的附加信息。这种信息损失由抽象函数描述,抽象函数是从具体值空间到抽象空间的映射。实现 ListSet 的抽象函数如下所示:

af-listset

Notice that several concrete values may map to a single abstract value; that is, the abstraction function may be many-to-one. It is also possible that some concrete values do not map to any abstract value; the abstraction function may be partial. That is not the case with ListSet, but it might be with other implementations.
请注意,多个具体值可能映射到单个抽象值;也就是说,抽象函数可以是多对一的。某些具体值也可能不映射到任何抽象值;抽象函数可能是部分的。 ListSet 的情况并非如此,但其他实现可能是这样。

The abstraction function is important for deciding whether an implementation is correct, therefore it belongs as a comment in the implementation of any abstract data type. For example, in the ListSet module, we could document the abstraction function as follows:
抽象函数对于决定实现是否正确非常重要,因此它属于任何抽象数据类型的实现中的注释。例如,在 ListSet 模块中,我们可以将抽象函数记录如下:

module ListSet : Set = struct
  (** Abstraction function: The list [[a1; ...; an]] represents the
      set [{b1, ..., bm}], where [[b1; ...; bm]] is the same list as
      [[a1; ...; an]] but with any duplicates removed. The empty list
      [[]] represents the empty set [{}]. *)
  type 'a t = 'a list
  ...
end

This comment explicitly points out that the list may contain duplicates, which is helpful as a reinforcement of the first sentence. Similarly, the case of an empty list is mentioned explicitly for clarity, although some might consider it to be redundant.
此注释明确指出该列表可能包含重复项,这对于强化第一句话很有帮助。同样,为了清楚起见,明确提到了空列表的情况,尽管有些人可能认为这是多余的。

The abstraction function for the second implementation, which does not allow duplicates, hints at an important difference. We can write the abstraction function for this second representation a bit more simply because we know that the elements are distinct.
第二个实现的抽象函数不允许重复,暗示了一个重要的区别。我们可以更简单地编写第二个表示的抽象函数,因为我们知道元素是不同的。

module UniqListSet : Set = struct
  (** Abstraction function: The list [[a1; ...; an]] represents the set
      [{a1, ..., an}]. The empty list [[]] represents the empty set [{}]. *)
  type 'a t = 'a list
  ...
end

6.3.2. Implementing the Abstraction Function
6.3.2. 实现抽象函数 ¶

What would it mean to implement the abstraction function for ListSet? We’d want a function that took an input of type 'a ListSet.t. But what should its output type be? The abstract values are mathematical sets, not OCaml types. If we did hypothetically have a type 'a set that our abstraction function could return, there would have been little point in developing ListSet; we could have just used that 'a set type without doing any work of our own.
实现 ListSet 的抽象函数意味着什么?我们想要一个接受 'a ListSet.t 类型输入的函数。但它的输出类型应该是什么?抽象值是数学集,而不是 OCaml 类型。如果我们假设我们的抽象函数确实有一个类型 'a set 可以返回,那么开发 ListSet 就没有什么意义了;我们可以只使用 'a set 类型,而不需要做任何我们自己的工作。

On the other hand, we might implement something close to the abstraction function by converting an input of type 'a ListSet.t to a built-in OCaml type or standard library type:
另一方面,我们可以通过将 'a ListSet.t 类型的输入转换为内置 OCaml 类型或标准库类型来实现类似于抽象函数的功能:

  • We could convert to a string. That would have the advantage of being easily readable by humans in the toplevel or in debug output. Java programmers use toString() for similar purposes.
    我们可以转换为 string 。这样做的优点是可以在顶层或调试输出中轻松地被人类读取。 Java 程序员使用 toString() 来实现类似的目的。

  • We could convert to 'a list. (Actually there’s little conversion to be done). For data collections this is a convenient choice, since lists can at least approximately represent many data structures: stacks, queues, dictionaries, sets, heaps, etc.
    我们可以转换为 'a list 。 (实际上几乎不需要做任何转换)。对于数据集合来说,这是一个方便的选择,因为列表至少可以近似地表示许多数据结构:堆栈、队列、字典、集合、堆等。

The following functions implement those ideas. Note that to_string has to take an additional argument string_of_val from the client to convert 'a to string.
以下函数实现了这些想法。请注意, to_string 必须从客户端获取附加参数 string_of_val 才能将 'a 转换为 string

module ListSet : Set = struct
  ...

  let uniq lst = List.sort_uniq Stdlib.compare lst

  let to_string string_of_val lst =
    let interior =
      lst |> uniq |> List.map string_of_val |> String.concat ", "
    in
    "{" ^ interior ^ "}"

  let to_list = uniq
end

Installing a custom formatter, as discussed in the section on encapsulation, could also be understood as implementing the abstraction function. But in that case it’s usable only by humans at the toplevel rather than other code, programmatically.
正如封装部分所讨论的,安装自定义格式化程序也可以理解为实现抽象功能。但在这种情况下,它只能由顶层人员而不是其他代码以编程方式使用。

6.3.3. Commutative Diagrams
6.3.3. 交换图 ¶

Using the abstraction function, we can now talk about what it means for an implementation of an abstraction to be correct. It is correct exactly when every operation that takes place in the concrete space makes sense when mapped by the abstraction function into the abstract space. This can be visualized as a commutative diagram:
使用抽象函数,我们现在可以讨论抽象实现的正确性意味着什么。当具体空间中发生的每个操作被抽象函数映射到抽象空间时才有意义时,这就是正确的。这可以可视化为交换图:

commutative-diagram

A commutative diagram means that if we take the two paths around the diagram, we have to get to the same place. Suppose that we start from a concrete value and apply the actual implementation of some operation to it to obtain a new concrete value or values. When viewed abstractly, a concrete result should be an abstract value that is a possible result of applying the function as described in its specification to the abstract view of the actual inputs. For example, consider the union function from the implementation of sets as lists with repeated elements covered last time. When this function is applied to the concrete pair [1; 3], [2; 2], it corresponds to the lower-left corner of the diagram. The result of this operation is the list [2; 2; 1; 3], whose corresponding abstract value is the list {1, 2, 3}. Note that if we apply the abstraction function AF to the input lists [1; 3] and [2; 2], we have the sets {1, 3} and {2}. The commutative diagram requires that in this instance the union of {1, 3} and {2} is {1, 2, 3}, which is of course true.
交换图意味着如果我们沿着图走两条路径,我们必须到达同一个地方。假设我们从一个具体值开始,对其应用某些操作的实际实现,以获得一个或多个新的具体值。当抽象地看待时,具体结果应该是一个抽象值,它是将其规范中描述的函数应用到实际输入的抽象视图的可能结果。例如,考虑将集合实现为具有上次覆盖的重复元素的列表的并集函数。当此函数应用于具体对 [1; 3], [2; 2] ,它对应于图的左下角。该操作的结果是列表 [2; 2; 1; 3] ,其对应的抽象值为列表 {1,2,3} 。请注意,如果我们将抽象函数 AF 应用于输入列表 [1; 3] 和 [2; 2] ,我们有集合 {1, 3} 和 {2} 。交换图要求在这种情况下 {1, 3} 和 {2} 的并集是 {1, 2, 3} ,这当然是正确的。

6.3.4. Representation Invariants
6.3.4. 表征不变式 ¶

The abstraction function explains how information within the module is viewed abstractly by module clients. But that is not all we need to know to ensure correctness of the implementation. Consider the size function in each of the two implementations. For ListSet, which allows duplicates, we need to be sure not to double-count duplicate elements:
抽象函数解释了模块客户端如何抽象地查看模块内的信息。但这并不是我们确保实施正确性所需要知道的全部。考虑两个实现中的 size 函数。对于允许重复的 ListSet ,我们需要确保不要重复计算重复元素:

let size lst = List.(lst |> sort_uniq Stdlib.compare |> length)

But for UniqListSet, in which the lists have no duplicates, the size is just the length of the list:
但对于 UniqListSet ,其中列表没有重复项,大小只是列表的长度:

let size = List.length

How do wo know that latter implementation is correct? That is, how do we know that “lists have no duplicates”? It’s hinted at by the name of the module, and it can be deduced from the implementation of add, but we’ve never carefully documented it. Right now, the code does not explicitly say that there are no duplicates.
我们怎么知道后一个实现是正确的?也就是说,我们如何知道“列表没有重复项”?它由模块的名称暗示,并且可以从 add 的实现中推断出来,但我们从未仔细记录过它。目前,代码并未明确表示不存在重复项。

In the UniqListSet representation, not all concrete data items represent abstract data items. That is, the domain of the abstraction function does not include all possible lists. There are some lists, such as [1; 1; 2], that contain duplicates and must never occur in the representation of a set in the UniqListSet implementation; the abstraction function is undefined on such lists. We need to include a second piece of information, the representation invariant (or rep invariant, or RI), to determine which concrete data items are valid representations of abstract data items. For sets represented as lists without duplicates, we write this as part of the comment together with the abstraction function:
UniqListSet 表示中,并非所有具体数据项都表示抽象数据项。也就是说,抽象函数的域不包括所有可能的列表。有一些列表,例如 [1; 1; 2] ,包含重复项,并且绝不能出现在 UniqListSet 实现中的集合表示中;此类列表上的抽象函数未定义。我们需要包含第二条信息,即表征的不变性(或表征不变式,或 RI ),以确定哪些具体数据项是抽象数据项的有效表示。对于表示为没有重复项的列表的集合,我们将其与抽象函数一起写为注释的一部分:

module UniqListSet : Set = struct
  (** Abstraction function: the list [[a1; ...; an]] represents the set
      [{a1, ..., an}].  The empty list [[]] represents the empty set [{}].
      Representation invariant: the list contains no duplicates. *)
  type 'a t = 'a list
  ...
end

If we think about this issue in terms of the commutative diagram, we see that there is a crucial property that is necessary to ensure correctness: namely, that all concrete operations preserve the representation invariant. If this constraint is broken, functions such as size will not return the correct answer. The relationship between the representation invariant and the abstraction function is depicted in this figure:
如果我们从交换图的角度考虑这个问题,我们会发现有一个关键属性是确保正确性所必需的:即所有具体操作都保持表征不变性。如果这个约束被打破,诸如 size 之类的函数将不会返回正确的答案。表症不变式和抽象函数之间的关系如下图所示:

af-and-ri

We can use the rep invariant and abstraction function to judge whether the implementation of a single operation is correct in isolation from the rest of the functions in the module. A function is correct if these conditions:
我们可以使用表示不变式和抽象函数来独立于模块中的其余函数来判断单个操作的实现是否正确。如果满足以下条件,则函数是正确的:

  1. The function’s preconditions hold of the argument values.
    函数的前提条件保存参数值。

  2. The concrete representations of the arguments satisfy the rep invariant.
    参数的具体表症满足表症不变性。

imply these conditions: 暗示这些条件:

  1. All new representation values created satisfy the rep invariant.
    创建的所有新表示值都满足表征不变性。

  2. The commutative diagram holds.
    交换图成立。

The rep invariant makes it easier to write code that is provably correct, because it means that we don’t have to write code that works for all possible incoming concrete representations—only those that satisfy the rep invariant. For example, in the implementation UniqListSet, we do not care what the code does on lists that contain duplicate elements. However, we do need to be concerned that on return, we only produce values that satisfy the rep invariant. As suggested in the figure above, if the rep invariant holds for the input values, then it should hold for the output values, which is why we call it an invariant.
表症不变性使编写可证明正确的代码变得更加容易,因为这意味着我们不必编写适用于所有可能传入的具体表示的代码,只需编写满足表症不变式的代码即可。例如,在实现 UniqListSet 中,我们不关心代码对包含重复元素的列表执行的操作。然而,我们确实需要担心,在返回时,我们只产生满足表示不变式的值。正如上图所示,如果表征不变式对于输入值成立,那么它对于输出值也应该成立,这就是我们称之为不变式的原因。

6.3.5. Implementing the Representation Invariant
6.3.5. 实现表征不变式 ¶

When implementing a complex abstract data type, it is often helpful to write an internal function that can be used to check that the rep invariant holds of a given data item. By convention we will call this function rep_ok. If the module accepts values of the abstract type that are created outside the module, say by exposing the implementation of the type in the signature, then rep_ok should be applied to these to ensure the representation invariant is satisfied. In addition, if the implementation creates any new values of the abstract type, rep_ok can be applied to them as a sanity check. With this approach, bugs are caught early, and a bug in one function is less likely to create the appearance of a bug in another.
在实现复杂的抽象数据类型时,编写一个内部函数通常很有帮助,该函数可用于检查给定数据项的表征不变式是否成立。按照惯例,我们将此函数称为 rep_ok 。如果模块接受在模块外部创建的抽象类型的值(例如通过在签名中公开类型的实现),则应将 rep_ok 应用于这些值以确保满足表征不变式。此外,如果实现创建了抽象类型的任何新值,则可以将 rep_ok 应用于它们作为健全性检查。通过这种方法,可以及早发现错误,并且一个函数中的错误不太可能在另一个函数中产生错误。

A convenient way to write rep_ok is to make it an identity function that just returns the input value if the rep invariant holds and raises an exception if it fails.
编写 rep_ok 的一种便捷方法是使其成为一个恒等函数,如果表示不变式成立,则仅返回输入值,如果失败则引发异常。

(* Checks whether x satisfies the representation invariant. *)
let rep_ok x =
  if (* check the RI holds of x *) then x else failwith "RI violated"

Here is an implementation of Set that uses the same data representation as UniqListSet, but includes copious rep_ok checks. Note that rep_ok is applied to all input sets and to any set that is ever created. This ensures that if a bad set representation is created, it will be detected immediately. In case we somehow missed a check on creation, we also apply rep_ok to incoming set arguments. If there is a bug, these checks will help us quickly figure out where the rep invariant is being broken.
下面是 Set 的实现,它使用与 UniqListSet 相同的数据表示形式,但包含大量的 rep_ok 检查。请注意, rep_ok 应用于所有输入集以及曾经创建的任何集。这确保了如果创建了错误的集合表示,它将立即被检测到。如果我们以某种方式错过了对创建的检查,我们还将 rep_ok 应用于传入的设置参数。如果存在错误,这些检查将帮助我们快速找出表症不变式被破坏的位置。

(** Implementation of sets as lists without duplicates. *)
module UniqListSet : Set = struct

  (** Abstraction function: The list [[a1; ...; an]] represents the
      set {a1, ..., an}. The empty list [[]] represents the empty set [{}].
      Representation invariant: the list contains no duplicates. *)
  type 'a t = 'a list

  let rep_ok lst =
    let u = List.sort_uniq Stdlib.compare lst in
    match List.compare_lengths lst u with 0 -> lst | _ -> failwith "RI"

  let empty = []

  let mem x lst = List.mem x (rep_ok lst)

  let add x lst = rep_ok (if mem x (rep_ok lst) then lst else x :: lst)

  let rem x lst = rep_ok (List.filter (( <> ) x) (rep_ok lst))

  let size lst = List.length (rep_ok lst)

  let union lst1 lst2 =
    rep_ok
      (List.fold_left
         (fun u x -> if mem x lst2 then u else x :: u)
         (rep_ok lst2) (rep_ok lst1))

  let inter lst1 lst2 = rep_ok (List.filter (fun h -> mem h lst2) (rep_ok lst1))
end
module UniqListSet : Set

Calling rep_ok on every argument can be too expensive for the production version of a program. The rep_ok above, for example, requires linearithmic time, which destroys the efficiency of all the previously constant time or linear time operations. For production code, it may be more appropriate to use a version of rep_ok that only checks the parts of the rep invariant that are cheap to check. When there is a requirement that there be no run-time cost, rep_ok can be changed to an identity function (or macro) so the compiler optimizes away the calls to it. However, it is a good idea to keep around the full code of rep_ok so it can be easily reinstated during future debugging:
对于程序的生产版本来说,对每个参数调用 rep_ok 的成本可能太高。例如,上面的 rep_ok 需要线性时间,这破坏了之前所有恒定时间或线性时间操作的效率。对于生产代码,使用 rep_ok 版本可能更合适,该版本仅检查表征不变式中检查成本较低的部分。当要求没有运行时成本时,可以将 rep_ok 更改为恒等函数(或宏),以便编译器优化对其的调用。但是,最好保留 rep_ok 的完整代码,以便在将来的调试期间可以轻松恢复它:

let rep_ok lst = lst

let rep_ok_expensive =
  let u = List.sort_uniq Stdlib.compare lst in
  match List.compare_lengths lst u with 0 -> lst | _ -> failwith "RI"

Some languages provide support for conditional compilation, which provides some kind of support for compiling some parts of the codebase but not others. The OCaml compiler supports a flag noassert that disables assertion checking. So you could implement rep invariant checking with assert, and turn it off with noassert. The problem with that is that some portions of your codebase might require assertion checking to be turned on to work correctly.
某些语言提供对条件编译的支持,条件编译为编译代码库的某些部分提供某种支持,但不提供其他部分的支持。 OCaml 编译器支持禁用断言检查的标志 noassert 。因此,您可以使用 assert 实现表示不变式检查,并使用 noassert 将其关闭。问题是代码库的某些部分可能需要打开断言检查才能正常工作。

6.4. Testing and Debugging
6.4. 检测和调试 ¶

Correct programs behave as we intend them to behave. Validation is the process of building our confidence in correct program behavior.
正确的程序会按照我们的预期运行。验证是建立我们对正确程序行为的信心的过程。

6.4.1. Validation 6.4.1. 验证 ¶

There are many ways to increase that confidence. Social methods, formal methods, and testing are three. The latter is our main focus, but let’s first consider the other two.
有很多方法可以增强这种信心。社会方法、正式方法和测试是三种方法。后者是我们的主要关注点,但我们首先考虑其他两个。

Social methods involve developing programs with other people, relying on their assistance to improve correctness. Some good techniques include the following:
社会方法涉及与其他人一起开发程序,依靠他们的帮助来提高正确性。一些好的技术包括:

  • Code walkthrough. In the walkthrough approach, the programmer presents the documentation and code to a reviewing team, and the team gives comments. This is an informal process. The focus is on the code rather than the coder, so hurt feelings are easier to avoid. However, the team may not get as much assurance that the code is correct.
    代码演练。在演练方法中,程序员向审核团队提供文档和代码,然后团队给出评论。这是一个非正式的过程。重点是代码而不是编码人员,因此更容易避免伤害感情。然而,团队可能无法充分保证代码的正确性。

  • Code inspection. Here, the review team drives the code review process. Some, though not necessarily very much, team preparation beforehand is useful. They define goals for the review process and interact with the coder(s) to understand where there may be quality problems. Again, making the process as blameless as possible is important.
    代码检查。在这里,审查团队推动代码审查过程。团队事先的一些准备(尽管不一定很多)是有用的。他们定义审查过程的目标,并与编码人员互动,以了解哪里可能存在质量问题。再次强调,使整个过程尽可能无可指责非常重要。

  • Pair programming. The most informal approach to code review is through pair programming, in which code is developed by a pair of engineers: the driver who writes the code, and the observer who watches. The role of the observer is be a critic, to think about potential errors, and to help navigate larger design issues. It’s usually better to have the observer be the engineer with the greater experience with the coding task at hand. The observer reviews the code, serving as the devil’s advocate that the driver must convince. When the pair is developing specifications, the observer thinks about how to make specs clearer or shorter. Pair programming has other benefits. It is often more fun and educational to work with a partner, and it helps focus both partners on the task. If you are just starting to work with another programmer, pair programming is a good way to understand how your partner thinks and to establish common vocabulary. It is a good idea for partners to trade off roles, too.
    结对编程。最非正式的代码审查方法是通过结对编程,其中代码由一对工程师开发:编写代码的驱动程序和观察代码的观察者。观察者的角色是批评家,思考潜在的错误,并帮助解决更大的设计问题。通常最好让观察者是对手头的编码任务有丰富经验的工程师。观察者审查代码,充当驱动程序必须说服的魔鬼代言人。当两人制定规范时,观察者会考虑如何使规范更清晰或更短。结对编程还有其他好处。与合作伙伴一起工作通常更有趣、更有教育意义,并且有助于双方集中精力完成任务。如果您刚刚开始与另一位程序员合作,结对编程是了解合作伙伴的想法并建立共同词汇的好方法。对于合作伙伴来说,权衡角色也是一个好主意。

These social techniques for code review can be remarkably effective. In one study conducted at IBM (Jones, 1991), code inspection found 65% of the known coding errors and 25% of the known documentation errors, whereas testing found only 20% of the coding errors and none of the documentation errors. The code inspection process may be more effective than walkthroughs. One study (Fagan, 1976) found that code inspections resulted in code with 38% fewer failures, compared to code walkthroughs.
这些用于代码审查的社交技术非常有效。在 IBM 进行的一项研究中(Jones,1991),代码检查发现了 65% 的已知编码错误和 25% 的已知文档错误,而测试仅发现了 20% 的编码错误,并且没有发现任何文档错误。代码检查过程可能比演练更有效。一项研究(Fagan,1976)发现,与代码走查相比,代码检查使代码的失败率减少了 38%。

Thorough code review can be expensive, however. Jones found that preparing for code inspection took one hour per 150 lines of code, and the actual inspection covered 75 lines of code per hour. Having up to three people on the inspection team improves the quality of inspection; beyond that, more inspectors doesn’t seem to help. Spending a lot of time preparing for inspection did not seem to be useful, either. Perhaps this is because much of the value of inspection lies in the interaction with the coders.
然而,彻底的代码审查可能会很昂贵。 Jones 发现,准备代码检查每 150 行代码需要花费一小时,而实际检查每小时覆盖 75 行代码。检查组人数最多为三人,可提高检查质量;除此之外,增加检查员似乎也无济于事。花大量时间准备检查似乎也没有什么用。也许这是因为检查的大部分价值在于与编码员的交互。

Formal methods use the power of mathematics and logic to validate program behavior. Verification uses the program code and its specifications to construct a proof that the program behaves correctly on all possible inputs. There are research tools available to help with program verification, often based on automated theorem provers, as well as research languages that are designed for program verification. Verification tends to be expensive and to require thinking carefully about and deeply understanding the code to be verified. So in practice, it tends to be applied to code that is important and relatively short. Verification is particularly valuable for critical systems where testing is less effective. Because their execution is not deterministic, concurrent programs are hard to test, and sometimes subtle bugs can only be found by attempting to verify the code formally. In fact, tools to help prove programs correct have been getting increasingly effective and some large systems have been fully verified, including compilers, processors and processor emulators, and key pieces of operating systems.
形式化方法利用数学和逻辑的力量来验证程序行为。验证使用程序代码及其规范来构建程序在所有可能的输入上正确运行的证明。有一些研究工具可以帮助进行程序验证,通常基于自动定理证明器,以及专为程序验证而设计的研究语言。验证往往成本高昂,并且需要仔细思考并深入理解要验证的代码。所以在实践中,它往往适用于重要且相对较短的代码。验证对于测试效率较低的关键系统尤其有价值。因为它们的执行是不确定的,所以并发程序很难测试,有时只有通过尝试正式验证代码才能发现细微的错误。事实上,帮助证明程序正确性的工具已经变得越来越有效,一些大型系统已经得到充分验证,包括编译器、处理器和处理器模拟器以及操作系统的关键部分。

Testing involves actually executing the program on sample inputs to see whether the behavior is as expected. By comparing the actual results of the program with the expected results, we find out whether the program really works on the particular inputs we try it on. Testing can never provide the absolute guarantees that formal methods do, but it is significantly easier and cheaper to do. It is also the validation methodology with which you are probably most familiar. Testing is a good, cost-effective way of building confidence in correct program behavior.
测试涉及对示例输入实际执行程序,以查看行为是否符合预期。通过将程序的实际结果与预期结果进行比较,我们可以了解该程序是否真正适用于我们尝试的特定输入。测试永远无法提供正式方法所提供的绝对保证,但它的实现要容易得多,成本也低得多。这也是您可能最熟悉的验证方法。测试是建立对正确程序行为的信心的一种经济高效的好方法。

6.4.2. Debugging 6.4.2. 调试 ¶

When testing reveals an error, we usually say that the program is “buggy”. But the word “bug” suggests something that wandered into a program. Better terminology would be that there are
当测试发现错误时,我们通常说该程序“buggy”了。但“bug”这个词暗示着程序中出现了一些东西。更好的术语是有

  • faults, which are the result of human errors in software systems, and
    故障,这是由于软件系统中的人为错误造成的,以及

  • failures, which are violations of requirements.
    失败,即违反要求。

Some faults might never appear to an end user of a system, but failures are those faults that do. A fault might result because an implementation doesn’t match design, or a design doesn’t match the requirements.
有些故障可能永远不会出现在系统的最终用户面前,但故障就是那些出现的故障。错误的产生可能是因为实现与设计不匹配,或者设计与需求不匹配。

Debugging is the process of discovering and fixing faults. Testing clearly is the “discovery” part, but fixing can be more complicated. Debugging can be a task that takes even more time than an original implementation itself! So you would do well to make it easy to debug your programs from the start. Write good specifications for each function. Document the AF and RI for each data abstraction. Keep modules small, and test them independently.
调试是发现并修复故障的过程。测试显然是“发现”部分,但修复可能会更复杂。调试可能是一项比原始实现本身花费更多时间的任务!因此,您最好从一开始就让调试程序变得容易。为每个功能编写良好的规范。记录每个数据抽象的 AF 和 RI。保持模块较小,并独立测试它们。

Inevitably, though, you will discover faults in your programs. When you do, approach them as a scientist by employing the scientific method:
但不可避免的是,您会发现程序中的错误。当你这样做时,通过采用科学方法像科学家一样接近他们:

  • evaluate the data that are available;
    评估可用数据;

  • formulate a hypothesis that might explain the data;
    提出一个可以解释数据的假设;

  • design a repeatable experiment to test that hypothesis; and
    设计一个可重复的实验来检验该假设;和

  • use the result of that experiment to refine or refute your hypothesis.
    使用该实验的结果来完善或反驳你的假设。

Often the crux of this process is finding the simplest, smallest input that triggers a fault. That’s not usually the original input for which we discover a fault. So some initial experimentation might be needed to find a minimal test case.
通常这个过程的关键是找到触发故障的最简单、最小的输入。这通常不是我们发现错误的原始输入。因此,可能需要进行一些初步实验来找到最小的测试用例。

Never be afraid to write additional code, even a lot of additional code, to help you find faults. Functions like to_string or format can be invaluable in understanding computations, so writing them up front before any faults are detected is completely worthwhile.
永远不要害怕编写额外的代码,甚至是很多额外的代码,来帮助你发现错误。像 to_stringformat 这样的函数对于理解计算非常有价值,因此在检测到任何错误之前预先编写它们是完全值得的。

When you do discover the source of a fault, be extra careful in fixing it. It is tempting to slap a quick fix into the code and move on. This is quite dangerous. Far too often, fixing a fault just introduces a new (and unknown) fault! If a bug is difficult to find, it is often because the program logic is complex and hard to reason about. Think carefully about why the fault could have been introduced in the first place, and about how you might prevent similar faults in the future.
当您确实发现故障根源时,在修复它时要格外小心。人们很容易对代码进行快速修复并继续前进。这是相当危险的。很多时候,修复故障只会引入新的(且未知的)故障!如果一个bug很难发现,往往是因为程序逻辑复杂,难以推理。首先仔细考虑为什么会出现该故障,以及如何防止将来出现类似的故障。

6.5. Black-box and Glass-box Testing
6.5. 黑盒白盒测试 ¶

We would like to know that a program works on all possible inputs. The problem with testing is that it is usually infeasible to try all the possible inputs. For example, suppose that we are implementing a module that provides an abstract data type for rational numbers. One of its operations might be an addition function plus, e.g.:
我们想知道一个程序适用于所有可能的输入。测试的问题在于尝试所有可能的输入通常是不可行的。例如,假设我们正在实现一个为有理数提供抽象数据类型的模块。它的操作之一可能是加法函数 plus ,例如:

module type RATIONAL = sig
  (** A [t] is a rational. *)
  type t

  (** [create p q] is the rational number [p/q].
      Raises: [Invalid_argument "0"] if [q] is 0. *)
  val create : int -> int -> t

  (** [plus r1 r2] is [r1 + r2] *)
  val plus : t -> t -> t
end

module Rational : RATIONAL = struct
  (** AF: [(p, q)] represents the rational number [p/q]
      RI: [q] is not 0. *)
  type t = int * int

  let create p q =
    if q = 0 then invalid_arg "0" else (p, q)

  let plus (p1, q1) (p2, q2) =
    (p1 * q2 + p2 * q1, q1 * q2)
end
module type RATIONAL =
  sig type t val create : int -> int -> t val plus : t -> t -> t end
module Rational : RATIONAL

What would it take to exhaustively test just this one function? We’d want to try all possible rationals as both the r1 and r2 arguments. A rational is formed from two ints, and there are 263 ints on a modern OCaml implementation. Therefore there are approximately (263)4=2252 possible inputs to the plus function. Even if we test one addition every nanosecond, it will take about 1059 years to finish testing this one function.
彻底测试这一功能需要什么?我们想要尝试所有可能的理性作为 r1r2 参数。有理数由两个整数组成,现代 OCaml 实现中有 263 个整数。因此, plus 函数大约有 (263)4=2252 个可能的输入。即使我们每纳秒测试一次添加,也需要大约 1059 年才能完成这一项功能的测试。

Clearly we can’t test software exhaustively. But that doesn’t mean we should give up on testing. It just means that we need to think carefully about what our test cases should be so that they are as effective as possible at convincing us that the code works.
显然我们无法彻底测试软件。但这并不意味着我们应该放弃测试。这只是意味着我们需要仔细考虑我们的测试用例应该是什么,以便它们尽可能有效地让我们相信代码是有效的。

Consider our create function, above. It takes in two integers p and q as arguments. How should we go about selecting a relatively small number of test cases that will convince us that the function works correctly on all possible inputs? We can visualize the space of all possible inputs as a large square:
考虑上面我们的 create 函数。它接受两个整数 pq 作为参数。我们应该如何选择相对较少数量的测试用例来让我们相信该函数在所有可能的输入上都能正常工作?我们可以将所有可能输入的空间可视化为一个大正方形:

There are about 2126 points in this square, so we can’t afford to test them all. And testing them all is going to mostly be a waste of time—most of the possible inputs provide nothing new. We need a way to find a set of points in this space to test that are interesting and will give a good sense of the behavior of the program across the whole space.
这个正方形中大约有 2126 个点,因此我们无法全部测试它们。对它们进行全部测试基本上是浪费时间——大多数可能的输入都没有提供任何新内容。我们需要一种方法来在这个空间中找到一组有趣的点来进行测试,并且可以很好地了解程序在整个空间中的行为。

Input spaces generally comprise a number of subsets in which the behavior of the code is similar in some essential fashion across the entire subset. We don’t get any additional information by testing more than one input from each such subset.
输入空间通常包括多个子集,其中代码的行为在整个子集中以某种基本方式相似。通过测试每个此类子集中的多个输入,我们不会获得任何其他信息。

If we test all the interesting regions of the input space, we have achieved good coverage. We want tests that in some useful sense cover the space of possible program inputs.
如果我们测试输入空间的所有有趣区域,我们就获得了良好的覆盖率。我们希望测试能够在某种有用的意义上覆盖可能的程序输入空间。

Two good ways of achieving coverage are black-box testing and glass-box testing. We discuss those, next.
实现覆盖的两种好方法是黑盒测试玻璃盒测试。接下来我们讨论这些。

6.5.1. Black-box Testing
6.5.1. 黑盒测试 ¶

In selecting our test cases for good coverage, we might want to consider both the specification and the implementation of the program or module being tested. It turns out that we can often do a pretty good job of picking test cases by just looking at the specification and ignoring the implementation. This is known as black-box testing. The idea is that we think of the code as a black box about which all we can see is its surface: its specification. We pick test cases by looking at how the specification implicitly introduces boundaries that divide the space of possible inputs into different regions.
在选择测试用例以获得良好的覆盖范围时,我们可能需要考虑正在测试的程序或模块的规范和实现。事实证明,我们通常可以通过仅查看规范并忽略实现来很好地选择测试用例。这称为黑盒测试。这个想法是,我们将代码视为一个黑匣子,我们只能看到它的表面:它的规范。我们通过查看规范如何隐式引入将可能输入的空间划分为不同区域的边界来选择测试用例。

When writing black-box test cases, we ask ourselves what set of test cases that will produce distinctive behavior as predicted by the specification. It is important to try out both typical inputs and inputs that are boundary cases aka corner cases or edge cases. A common error is to only test typical inputs, with the result that the program usually works but fails in less frequent situations. It’s also important to identify ways in which the specification creates classes of inputs that should elicit similar behavior from the function, and to test on those paths through the specification. Here are some examples.
在编写黑盒测试用例时,我们会问自己哪一组测试用例将产生规范所预测的独特行为。尝试典型输入和边界情况(又称角落情况或边缘情况)的输入非常重要。一个常见的错误是只测试典型的输入,结果是程序通常可以工作,但在不太频繁的情况下会失败。确定规范创建输入类的方式也很重要,这些输入类应该从函数中引发类似的行为,并通过规范对这些路径进行测试。这里有些例子。

Example 1. 示例 1.

Here are some ideas for how to test the create function:
以下是有关如何测试 create 函数的一些想法:

  • Looking at the square above, we see that it has boundaries at min_int and max_int. We want to try to construct rationals at the corners and along the sides of the square, e.g. create min_int min_int, create max_int 2, etc.
    查看上面的正方形,我们发现它的边界位于 min_intmax_int 处。我们想要尝试在正方形的角点和边上构建有理数,例如 create min_int min_intcreate max_int 2

  • The line p=0 is important because p/q is zero all along it. We should try (0, q) for various values of q.
    p=0 行很重要,因为 p/q 一直为零。我们应该尝试使用 (0, q) 来获取 q 的各种值。

  • We should try some typical (p, q) pairs in all four quadrants of the space.
    我们应该在空间的所有四个象限中尝试一些典型的 (p, q) 对。

  • We should try both (p, q) pairs in which q divides evenly into p, and pairs in which q does not divide into p.
    我们应该尝试 (p, q) 对,其中 q 均匀地分为 p ,以及 q 不分为 p

  • Pairs of the form (1, q), (-1, q), (p, 1), (p, -1) for various p and q also may be interesting given the properties of rational numbers.
    鉴于有理数的性质,对于不同的 pq ,像如 (1, q), (-1, q), (p, 1), (p, -1) 这样的形式对也可能很有趣。

The specification also says that the code will check that q is not zero. We should construct some test cases to ensure this checking is done as advertised. Trying (1, 0), (max_int, 0), (min_int, 0), (-1, 0), (0, 0) to see that they all raise the specified exception would probably be an adequate set of black-box tests.
规范还指出,代码将检查 q 是否不为零。我们应该构建一些测试用例以确保此检查按照广告完成。尝试 (1, 0)(max_int, 0)(min_int, 0)(-1, 0)(0, 0) 来查看它们是否都引发指定的异常可能是一组足够的黑盒测试。

Example 2. 示例 2.

Consider a function list_max:
考虑一个函数 list_max

(** Return the maximum element in the list. *)
val list_max: int list -> int

What is a good set of black-box test cases? Here the input space is the set of all possible lists of ints. We need to try some typical inputs and also consider boundary cases. Based on this spec, boundary cases include the following:
什么是一套好的黑盒测试用例?这里的输入空间是所有可能的整数列表的集合。我们需要尝试一些典型的输入并考虑边界情况。根据此规范,边界情况包括以下内容:

  • A list containing one element. In fact, an empty list is probably the first boundary case we think of. Looking at the spec above, we realize that it doesn’t specify what happens in the case of an empty list. Thus, thinking about boundary cases is also useful in identifying errors in the specification.
    包含一个元素的列表。事实上,空列表可能是我们想到的第一个边界情况。查看上面的规范,我们意识到它没有指定空列表情况下会发生什么。因此,考虑边界情况对于识别规范中的错误也很有用。

  • A list containing two elements.
    包含两个元素的列表。

  • A list in which the maximum is the first element. Or the last element. Or somewhere in the middle of the list.
    一个列表,其中最大值是第一个元素。或者最后一个元素。或者列表中间的某个位置。

  • A list in which every element is equal.
    每个元素都相等的列表。

  • A list in which the elements are arranged in ascending sorted order, and one in which they are arranged in descending sorted order.
    一种列表中的元素按升序排列,另一种列表中的元素按降序排列。

  • A list in which the maximum element is max_int, and a list in which the maximum element is min_int.
    最大元素为 max_int 的列表,以及最大元素为 min_int 的列表。

Example 3. 示例 3.

Consider the function sqrt:
考虑函数 sqrt

(** [sqrt x n] is the square root of [x] computed to an accuracy of [n]
    significant digits.
    Requires: [x >= 0] and [n >= 1]. *)
val sqrt : float -> int -> float

The precondition identifies two possibilities for x (either it is 0 or greater) and two possibilities for n (either it is 1 or greater). That leads to four “paths through the specification”, i.e., representative and boundary cases for satisfying the precondition, which we should test:
前提条件标识 x 的两种可能性(0 或更大)和 n 的两种可能性(1 或更大)。这导致了四个“通过规范的路径”,即满足先决条件的代表性和边界情况,我们应该测试它们:

  • x is 0 and n is 1
    x 为 0, n 为 1

  • x is greater than 0 and n is 1
    x 大于 0, n 为 1

  • x is 0 and n is greater than 1
    x 为 0, n 大于 1

  • x is greater than 0 and n is greater than 1.
    x 大于 0, n 大于 1。

6.5.2. Black-box Testing of Data Abstractions
6.5.2. 数据抽象的黑盒测试 ¶

So far we’ve been thinking about testing just one function at a time. But data abstractions usually have many operations, and we need to test how those operations interact with one another. It’s useful to distinguish consumer and producers of the data abstraction:
到目前为止,我们一直在考虑一次只测试一项功能。但数据抽象通常有很多操作,我们需要测试这些操作如何相互交互。区分数据抽象的消费者和生产者很有用:

  • A consumer is an operation that takes a value of the data abstraction as input.
    消费者是一种将数据抽象的值作为输入的操作。

  • A producer is an operation that returns a value of the data abstraction as output.
    生产者是一种返回数据抽象值作为输出的操作。

For example, consider this set abstraction:
例如,考虑这个集合抽象:

module type Set = sig

  (** ['a t] is the type of a set whose elements have type ['a]. *)
  type 'a t

  (** [empty] is the empty set. *)
  val empty : 'a t

  (** [size s] is the number of elements in [s]. *
      [size empty] is [0]. *)
  val size : 'a t -> int

  (** [add x s] is a set containing all the elements of
      [s] as well as element [x]. *)
  val add : 'a -> 'a t -> 'a t

  (** [mem x s] is [true] iff [x] is an element of [s]. *)
  val mem : 'a -> 'a t -> bool

end
module type Set =
  sig
    type 'a t
    val empty : 'a t
    val size : 'a t -> int
    val add : 'a -> 'a t -> 'a t
    val mem : 'a -> 'a t -> bool
  end

The empty and add functions are producers; and the size, add and mem functions are consumers.
emptyadd 函数是生产者; sizeaddmem 函数是消费者。

When black-box testing a data abstraction, we should test how each consumer of the data abstraction handles every path through each producer of it. In the Set example, that means testing the following:
当对数据抽象进行黑盒测试时,我们应该测试数据抽象的每个消费者如何处理通过其每个生产者的每条路径。在 Set 示例中,这意味着测试以下内容:

  • how size handles the empty set;
    size 如何处理 empty 集合;

  • how size handles a set produced by add, both when add leaves the set unchanged as well as when it increases the set;
    add 保持集合不变以及增加集合时, size 如何处理由 add 生成的集合;

  • how add handles sets produced by empty as well as add itself;
    add 如何处理 empty 以及 add 本身生成的集合;

  • how mem handles sets produced by empty as well as add, including paths where mem is invoked on elements that have been added as well as elements that have not.
    mem 如何处理 empty 以及 add 生成的集合,包括在已添加的元素上调用 mem 的路径作为没有的元素。

6.5.3. Glass-box Testing
6.5.3. 白盒测试 ¶

Black-box testing is a good place to start when writing test cases, but ultimately it is not enough. In particular, it’s not possible to determine how much coverage of the implementation a black-box test suite actually achieves—we actually need to know the implementation source code. Testing based on that code is known as glass box or white box testing. Glass-box testing can improve on black-box by testing execution paths through the implementation code: the series of expressions that is conditionally evaluated based on if-expressions, match-expressions, and function applications. Test cases that collectively exercise all paths are said to be path-complete. At a minimum, path-completeness requires that for every line of code, and even for every expression in the program, there should be a test case that causes it to be executed. Any unexecuted code could contain a bug if has never been tested.
黑盒测试是编写测试用例时的一个很好的起点,但最终还不够。特别是,不可能确定黑盒测试套件实际实现了多少实现覆盖率——我们实际上需要知道实现源代码。基于该代码的测试称为玻璃盒或白盒测试。玻璃盒测试可以通过测试实现代码的执行路径来改进黑盒测试:基于 if 表达式、匹配表达式和函数应用程序进行条件评估的一系列表达式。共同执行所有路径的测试用例被称为路径完整的。至少,路径完整性要求对于每一行代码,甚至对于程序中的每个表达式,都应该有一个导致其执行的测试用例。如果从未测试过,任何未执行的代码都可能包含错误。

For true path completeness we must consider all possible execution paths from start to finish of each function, and try to exercise every distinct path. In general this is infeasible, because there are too many paths. A good approach is to think of the set of paths as the space that we are trying to explore, and to identify boundary cases within this space that are worth testing.
为了真正的路径完整性,我们必须考虑每个函数从开始到结束的所有可能的执行路径,并尝试执行每个不同的路径。一般来说这是不可行的,因为路径太多。一个好的方法是将路径集视为我们试图探索的空间,并识别该空间内值得测试的边界情况。

For example, consider the following implementation of a function that finds the maximum of its three arguments:
例如,考虑以下函数的实现,该函数查找其三个参数中的最大值:

let max3 x y z =
  if x > y then
    if x > z then x else z
  else
    if y > z then y else z
val max3 : 'a -> 'a -> 'a -> 'a = <fun>

Black-box testing might lead us to invent many tests, but looking at the implementation reveals that there are only four paths through the code—the paths that return x, z, y, or z (again). We could test each of those paths with representative inputs such as: max3 3 2 1, max3 3 2 4, max3 1 2 1, max3 1 2 3.
黑盒测试可能会导致我们发明许多测试,但查看实现会发现代码中只有四个路径 - 返回 xzyz (再次)。我们可以使用代表性输入来测试每个路径,例如: max3 3 2 1max3 3 2 4max3 1 2 1max3 1 2 3

When doing glass box testing, we should include test cases for each branch of each (nested) if expression, and each branch of each (nested) pattern match. If there are recursive functions, we should include test cases for the base cases as well as each recursive call. Also, we should include test cases to trigger each place where an exception might be raised.
在进行玻璃盒测试时,我们应该包括每个(嵌套)if 表达式的每个分支以及每个(嵌套)模式匹配的每个分支的测试用例。如果存在递归函数,我们应该包括基本用例的测试用例以及每个递归调用。此外,我们应该包括测试用例来触发可能引发异常的每个地方。

Of course, path complete testing does not guarantee an absence of errors. We still need to test against the specification, i.e., do black-box testing. For example, here is a broken implementation of max3:
当然,路径完整测试并不能保证不存在错误。我们仍然需要根据规范进行测试,即进行黑盒测试。例如,下面是 max3 的一个损坏的实现:

let max3 x y z = x

The test max3 2 1 1 is path complete, but doesn’t reveal the error.
测试 max3 2 1 1 路径完整,但未显示错误。

6.5.4. Glass-box Testing of Data Abstractions
6.5.4. 数据抽象的玻璃盒测试 ¶

Look at the abstraction function and representation invariant for hints about what boundaries may exist in the space of values manipulated by a data abstraction. The rep invariant is a particularly effective tool for constructing useful test cases. Looking at the rep invariant of the Rational data abstraction above, we see that it requires that q is non-zero. Therefore we should construct test cases to see whether it’s possible to cause that invariant to be violated.
查看抽象函数和表示不变量,以获取有关数据抽象操作的值空间中可能存在哪些边界的提示。代表不变量是构建有用测试用例的特别有效的工具。查看上面 Rational 数据抽象的表示不变量,我们发现它要求 q 非零。因此,我们应该构建测试用例来看看是否有可能导致该不变量被违反。

6.5.5. Black-box vs. Glass-box
6.5.5. 黑盒 vs 白盒 ¶

Black-box testing has some important advantages:
黑盒测试有一些重要的优点:

  • It doesn’t require that we see the code we are testing. Sometimes code will not be available in source code form, yet we can still construct useful test cases without it. The person writing the test cases does not need to understand the implementation.
    它并不要求我们看到正在测试的代码。有时代码无法以源代码形式提供,但我们仍然可以在没有源代码的情况下构建有用的测试用例。编写测试用例的人不需要了解实现。

  • The test cases do not depend on the implementation. They can be written in parallel with or before the implementation. Further, good black-box test cases do not need to be changed, even if the implementation is completely rewritten.
    测试用例不依赖于实现。它们可以与实现并行或在实现之前编写。此外,即使完全重写实现,好的黑盒测试用例也不需要更改。

  • Constructing black-box test cases causes the programmer to think carefully about the specification and its implications. Many specification errors are caught this way.
    构建黑盒测试用例使程序员仔细思考规范及其含义。许多规范错误都是通过这种方式捕获的。

The disadvantage of black box testing is that its coverage may not be as high as we’d like, because it has to work without the implementation.
黑盒测试的缺点是它的覆盖率可能没有我们想要的那么高,因为它必须在没有实现的情况下工作。

6.5.6. Bisect 6.5.6. 二等分 ¶

Glass-box testing can be aided by code-coverage tools that assess how much of the code has been exercised by a test suite. The bisect_ppx tool for OCaml can tell you which expressions in your program have been tested, and which have not. Here’s how it works:
代码覆盖工具可以帮助玻璃盒测试,这些工具可以评估测试套件执行了多少代码。 OCaml 的 bisect_ppx 工具可以告诉您程序中的哪些表达式已经过测试,哪些还没有。它的工作原理如下:

  • You compile your code using Bisect_ppx (henceforth, just Bisect for short) as part of the compilation process. It instruments your code, mainly by inserting additional expressions to be evaluated.
    作为编译过程的一部分,您可以使用 Bisect_ppx(以下简称 Bisect)来编译代码。它主要通过插入要计算的附加表达式来检测您的代码。

  • You run your code. The instrumentation that Bisect inserted causes your program to do something in addition to whatever functionality you programmed yourself: the program will now record which expressions from the source code actually get executed at run time, and which do not. Also, the program will now produce an output file that contains that information.
    你运行你的代码。 Bisect 插入的检测使您的程序除了执行您自己编写的任何功能之外还执行一些操作:程序现在将记录源代码中的哪些表达式在运行时实际执行,哪些不执行。此外,该程序现在将生成一个包含该信息的输出文件。

  • You run a tool called bisect-ppx-report on that output file. It produces HTML showing you which parts of your code got executed, and which did not.
    您在该输出文件上运行一个名为 bisect-ppx-report 的工具。它会生成 HTML,显示代码的哪些部分已执行,哪些部分未执行。

How does that help with computing coverage of a test suite? If you run your OUnit test suite, the test cases in it will cause the code in whatever functions they test to be executed. If you don’t have enough test cases, some code in your functions will never be executed. The report produced by Bisect will show you exactly what code that is. You can then design new glass-box test cases to cause that code to execute, add them to your OUnit suite, and create a new Bisect report to confirm that the code really did get executed.
这对测试套件的计算覆盖率有何帮助?如果运行 OUnit 测试套件,其中的测试用例将导致执行它们测试的任何函数中的代码。如果您没有足够的测试用例,函数中的某些代码将永远不会被执行。 Bisect 生成的报告将准确显示该代码是什么。然后,您可以设计新的玻璃盒测试用例来执行该代码,将它们添加到您的 OUnit 套件中,并创建一个新的 Bisect 报告以确认代码确实得到执行。

Bisect Tutorial. 对分教程。

  1. Download the file sorts.ml. You will find an implementation of insertion sort and merge sort.
    下载文件sorts.ml。您将找到插入排序和合并排序的实现。

  2. Download the file test_sorts.ml. It has the skeleton for an OUnit test suite.
    下载文件 test_sorts.ml。它具有 OUnit 测试套件的框架。

  3. Create a dune file to execute test_sorts:
    创建一个 dune 文件来执行 test_sorts

    (executable
     (name test_sorts)
     (libraries ounit2)
     (instrumentation
      (backend bisect_ppx)))
    
  4. Run: 跑:

    $ dune exec --instrument-with bisect_ppx ./test_sorts.exe
    

    That will execute the test suite with Bisect coverage enabled, causing some files named bisectNNNN.coverage to be produced.
    这将执行启用 Bisect 覆盖的测试套件,从而生成一些名为 bisectNNNN.coverage 的文件。

  5. Run: 跑:

    $ bisect-ppx-report html
    

    to generate the Bisect report from your test suite execution. The report is in a newly-created directory named _coverage.
    从测试套件执行中生成 Bisect 报告。该报告位于新创建的名为 _coverage 的目录中。

  6. Open the file _coverage/index.html in a web browser. Look at the per-file coverage; you’ll see we’ve managed to test a few percent of sorts.ml with our test suite so far. Click on the link in that report for sorts.ml. You’ll see that we’ve managed to cover only one line of the source code.
    在网络浏览器中打开文件 _coverage/index.html 。查看每个文件的覆盖率;您会看到到目前为止,我们已经成功使用我们的测试套件测试了 sorts.ml 的百分之几。单击该报告中 sorts.ml 的链接。您会发现我们只覆盖了源代码的一行。

  7. There are some additional tests in the test file. Try un-commenting those, as documented in the test file, and increasing your code coverage. Between each run, you will need to delete the bisectNNNN.coverage files, otherwise the report will contain information from those previous runs:
    测试文件中有一些额外的测试。尝试取消注释这些内容(如测试文件中所述),并增加代码覆盖率。在每次运行之间,您需要删除 bisectNNNN.coverage 文件,否则报告将包含先前运行的信息:

    $ rm bisect*.coverage
    

By the time you’re done un-commenting the provided tests, you should be at 25% coverage, including all of the insertion sort implementation. For fun, try adding more tests to get 100% coverage of merge sort.
当您取消注释所提供的测试时,您的覆盖率应该达到 25%,包括所有插入排序实现。为了好玩,尝试添加更多测试以获得 100% 的合并排序覆盖率。

Parallelism. OUnit will by default attempt to run some of the tests in parallel, which reduces the time it takes to run a large test suite, at the tradeoff of making it nondeterministic in what order the tests run. It’s possible for that to affect coverage if you are testing imperative code. To make the tests run one at a time, in order, you can pass the flag -runner sequential to the executable. OUnit will see that flag and cease parallelization:
并行性。默认情况下,OUnit 将尝试并行运行一些测试,这会减少运行大型测试套件所需的时间,但代价是测试运行的顺序不确定。如果您正在测试命令式代码,这可能会影响覆盖率。要使测试按顺序一次运行一个,您可以将标志 -runner sequential 传递给可执行文件。 OUnit 将看到该标志并停止并行化:

$ dune exec --instrument-with bisect_ppx ./test_sorts.exe -- -runner sequential

6.6. Randomized Testing with QCheck
6.6. 用 QCheck 来随机测试 ¶

Randomized testing aka fuzz testing is the process of generating random inputs and feeding them to a program or a function to see whether the program behaves correctly. The immediate issue is how to determine what the correct output is for a given input. If a reference implementation is available—that is, an implementation that is believed to be correct but in some other way does not suffice (e.g., its performance is too slow, or it is in a different language)—then the outputs of the two implementations can be compared. Otherwise, perhaps some property of the output could be checked. For example,
随机测试又称为模糊测试,是生成随机输入并将其输入程序或函数以查看程序行为是否正确的过程。眼前的问题是如何确定给定输入的正确输出。如果有一个参考实现可用——也就是说,一个被认为是正确的实现,但在其他方面是不够的(例如,它的性能太慢,或者使用不同的语言)——那么两者的输出可以比较实现。否则,也许可以检查输出的某些属性。例如,

  • “not crashing” is a property of interest in user interfaces;
    “不崩溃”是用户界面中一个令人感兴趣的属性;

  • adding n elements to a data collection then removing those elements, and ending up with an empty collection, is a property of interest in data structures; and
    n 元素添加到数据集合中,然后删除这些元素,最后得到一个空集合,这是数据结构中令人感兴趣的属性;和

  • encrypting a string under a key then decrypting it under that key and getting back the original string is a property of interest in an encryption scheme like Enigma.
    使用密钥对字符串进行加密,然后使用该密钥对其进行解密并返回原始字符串,这是 Enigma 等加密方案中令人感兴趣的属性。

Randomized testing is an incredibly powerful technique. It is often used in testing programs for security vulnerabilities. The qcheck package for OCaml supports randomized testing. We’ll look at it, next, after we discuss random number generation.
随机测试是一种非常强大的技术。它经常用于测试程序的安全漏洞。 OCaml 的 qcheck 包支持随机测试。接下来,在我们讨论随机数生成之后,我们将讨论它。

6.6.1. Random Number Generation
6.6.1. 随机数生成 ¶

To understand randomized testing, we need to take a brief digression into random number generation.
为了理解随机测试,我们需要简单介绍一下随机数生成。

Most languages provide the facility to generate random numbers. In truth, these generators are usually not truly random (in the sense that they are completely unpredictable) but in fact are pseudorandom: the sequence of numbers they generate pass good statistical tests to ensure there is no discernible pattern in them, but the sequence itself is a deterministic function of an initial seed value. (Recall that the prefix pseudo is from the Greek pseudēs meaning “false”.) Java and Python both provide pseudorandom number generators (PRNGs). So does OCaml in the standard library’s Random module.
大多数语言都提供生成随机数的工具。事实上,这些生成器通常不是真正随机的(从某种意义上说,它们是完全不可预测的),但实际上是伪随机的:它们生成的数字序列通过了良好的统计测试,以确保其中没有可辨别的模式,但序列本身是初始种子值的确定性函数。 (回想一下,前缀pseudo 来自希腊语pseudēs,意思是“假”。)Java 和Python 都提供伪随机数生成器(PRNG)。标准库的 Random 模块中的 OCaml 也是如此。

An Experiment. Start a new session of utop and enter the following:
一个实验。启动 utop 的新会话并输入以下内容:

# Random.int 100;;
# Random.int 100;;
# Random.int 100;;

Each response will be an integer i such that 0i<100.
每个响应都是一个整数 i ,使得 0i<100

Now quit utop and start another new session. Enter the same phrases again. You will get the same responses as last time. In fact, unless your OCaml installation is somehow different than that used to produce this book, you will get the same numbers as those below:
现在退出 utop 并开始另一个新会话。再次输入相同的短语。您将得到与上次相同的回复。事实上,除非您的 OCaml 安装与本书中使用的安装有所不同,否则您将获得与以下相同的数字:

Random.int 100;;
Random.int 100;;
Random.int 100;;
- : int = 44
- : int = 85
- : int = 82

Not exactly unpredictable, eh?
并不完全不可预测,是吗?

PRNGs. Although for purposes of security and cryptography a PRNG leads to terrible vulnerabilities, for other purposes—including testing and simulation—PRNGs are just fine. Their predictability can even be useful: given the same initial seed, a PRNG will always produce the same sequence of pseudorandom numbers, leading to the ability to repeat a particular sequence of tests or a particular simulation.
伪随机数生成器。尽管出于安全和密码学的目的,PRNG 会导致严重的漏洞,但对于其他目的(包括测试和模拟),PRNG 就很好了。它们的可预测性甚至很有用:给定相同的初始种子,PRNG 将始终产生相同的伪随机数序列,从而能够重复特定的测试序列或特定的模拟。

The way a PRNG works in general is that it initializes a state that it keeps internally from the initial seed. From then on, each time the PRNG generates a new value, it imperatively updates that state. The Random module makes it possible to manipulate that state in limited ways. For example, you can
PRNG 的一般工作方式是从初始种子初始化一个在内部保存的状态。从那时起,每次 PRNG 生成新值时,它都会强制更新该状态。 Random 模块使得以有限的方式操纵该状态成为可能。例如,您可以

  • get the current state with Random.get_state,
    使用 Random.get_state 获取当前状态,

  • duplicate the current state with Random.State.copy,
    使用 Random.State.copy 复制当前状态,

  • request a random int generated from a particular state with Random.State.int, and
    使用 Random.State.int 请求从特定状态生成的随机 int ,以及

  • initialize the state yourself. The functions Random.self_init and Random.State.make_self_init will choose a “random” seed to initialize the state. They do so by sampling from a special Unix file named /dev/urandom, which is meant to provide as close to true randomness as a computer can.
    自己初始化状态。函数 Random.self_initRandom.State.make_self_init 将选择一个“随机”种子来初始化状态。他们通过从名为 /dev/urandom 的特殊 Unix 文件中进行采样来实现这一点,该文件旨在提供尽可能接近真实随机性的计算机功能。

Repeating the Experiment. Start a new session of utop. Enter the following:
重复实验。启动一个新的 utop 会话。输入以下内容:

# Random.self_init ();;
# Random.int 100;;
# Random.int 100;;
# Random.int 100;;

Now do that a second time (it doesn’t matter whether you exit utop or not in between). You will notice that you get a different sequence of values. With high probability, what you get will be different than the values below:
现在再做一次(中间是否退出 utop 并不重要)。您会注意到您得到了不同的值序列。您得到的值很有可能与以下值不同:

Random.self_init ();;
Random.int 100;;
Random.int 100;;
Random.int 100;;
- : unit = ()
- : int = 63
- : int = 45
- : int = 61

6.6.2. QCheck Abstractions
6.6.2. QCheck 抽象 ¶

QCheck has three abstractions we need to cover before using it for testing: generators, properties, and arbitraries. If you want to follow along in utop, load QCheck with this directive:
在使用 QCheck 进行测试之前,我们需要先了解三个抽象概念:生成器、属性和任意值。如果您想在 utop 中进行操作,请使用以下指令加载 QCheck:

#require "qcheck";;

Generators. One of the key pieces of functionality provided by QCheck is the ability to generate pseudorandom values of various types. Here is some of the signature of the module that does that:
生成器。 QCheck 提供的关键功能之一是能够生成各种类型的伪随机值。以下是执行此操作的模块的一些签名:

module QCheck : sig
  ...
  module Gen :
  sig
    type 'a t = Random.State.t -> 'a
    val int : int t
    val generate : ?rand:Random.State.t -> n:int -> 'a t -> 'a list
    val generate1 : ?rand:Random.State.t -> 'a t -> 'a
    ...
  end
  ...
end

An 'a QCheck.Gen.t is a function that takes in a PRNG state and uses it to produce a pseudorandom value of type 'a. So QCheck.Gen.int produces pseudorandom integers. The function generate1 actually does the generation of one pseudorandom value. It takes an optional argument that is a PRNG state; if that argument is not supplied, it uses the default PRNG state. The function generate produces a list of n pseudorandom values.
'a QCheck.Gen.t 是一个函数,它接受 PRNG 状态并使用它生成 'a 类型的伪随机值。因此 QCheck.Gen.int 产生伪随机整数。函数 generate1 实际上生成一个伪随机值。它采用一个可选参数,即 PRNG 状态;如果未提供该参数,则使用默认的 PRNG 状态。函数 generate 生成 n 伪随机值列表。

QCheck implements many producers of pseudorandom values. Here are a few more of them:
QCheck 实现了许多伪随机值的生成器。以下是其中的一些:

module QCheck : sig
  ...
  module Gen :
  sig
    val int : int t
    val small_int : int t
    val int_range : int -> int -> int t
    val list : 'a t -> 'a list t
    val list_size : int t -> 'a t -> 'a list t
    val string : ?gen:char t -> string t
    val small_string : ?gen:char t -> string t
    ...
  end
  ...
end

You can read the documentation of those and many others.
您可以阅读这些文档以及许多其他文档。

Properties. It’s tempting to think that QCheck would enable us to test a function by generating many pseudorandom inputs to the function, running the function on them, then checking that the outputs are correct. But there’s immediately a problem: how can QCheck know what the correct output is for each of those inputs? Since they’re randomly generated, the test engineer can’t hardcode the right outputs.
属性。我们很容易认为 QCheck 可以让我们通过生成函数的许多伪随机输入、在它们上运行函数、然后检查输出是否正确来测试函数。但马上就有一个问题:QCheck 如何知道每个输入的正确输出是什么?由于它们是随机生成的,测试工程师无法对正确的输出进行硬编码。

So instead, QCheck allows us to check whether a property of each output holds. A property is a function of type t -> bool, for some type t, that tells use whether the value of type t exhibits some desired characteristic. Here, for example, are two properties; one that determines whether an integer is even, and another that determines whether a list is sorted in non-decreasing order according to the built-in <= operator:
因此,QCheck 允许我们检查每个输出的属性是否成立。对于某些类型 t 来说,属性是 t -> bool 类型的函数,它告诉用户 t 类型的值是否表现出某些所需的特征。例如,这里有两个属性;一个确定整数是否为偶数,另一个确定列表是否根据内置的 <= 运算符按非降序排序:

let is_even n = n mod 2 = 0

let rec is_sorted = function
  | [] -> true
  | [ h ] -> true
  | h1 :: (h2 :: t as t') -> h1 <= h2 && is_sorted t'
val is_even : int -> bool = <fun>
val is_sorted : 'a list -> bool = <fun>

Arbitraries. The way we present to QCheck the outputs to be checked is with a value of type 'a QCheck.arbitrary. This type represents an “arbitrary” value of type 'a—that is, it has been pseudorandomly chosen as a value that we want to check, and more specifically, to check whether it satisfies a property.
任意。我们向 QCheck 呈现要检查的输出的方式是使用 'a QCheck.arbitrary 类型的值。该类型表示 'a 类型的“任意”值 - 也就是说,它已被伪随机选择为我们要检查的值,更具体地说,检查它是否满足属性。

We can create arbitraries out of generators using the function QCheck.make : 'a QCheck.Gen.t -> 'a QCheck.arbitrary. (Actually that function takes some optional arguments that we elide here.) This isn’t actually the normal way to create arbitraries, but it’s a simple way that will help us understand them; we’ll get to the normal way in a little while. For example, the following expression represents an arbitrary integer:
我们可以使用函数 QCheck.make : 'a QCheck.Gen.t -> 'a QCheck.arbitrary 从生成器中创建任意值。 (实际上,该函数接受一些我们在此处省略的可选参数。)这实际上不是创建任意值的正常方法,但这是一种帮助我们理解它们的简单方法;过一会儿我们就会恢复正常。例如,下面的表达式表示任意整数:

QCheck.make QCheck.Gen.int
- : int QCheck.arbitrary =
{QCheck.gen = <fun>; print = None; small = None; shrink = None;
 collect = None; stats = []}

6.6.3. Testing Properties
6.6.3. 测试属性 ¶

To construct a QCheck test, we create an arbitrary and a property, and pass them to QCheck.Test.make, whose type can be simplified to:
为了构建 QCheck 测试,我们创建一个任意值和一个属性,并将它们传递给 QCheck.Test.make ,其类型可以简化为:

QCheck.Test.make : 'a QCheck.arbitrary -> ('a -> bool) -> QCheck.Test.t

In reality, that function also takes several optional arguments that we elide here. The test will generate some number of arbitraries and check whether the property holds of each of them. For example, the following code creates a QCheck test that checks whether an arbitrary integer is even:
实际上,该函数还需要几个我们在这里省略的可选参数。该测试将生成一定数量的任意值,并检查每个任意值的属性是否成立。例如,以下代码创建一个 QCheck 测试,检查任意整数是否为偶数:

let t = QCheck.Test.make (QCheck.make QCheck.Gen.int) is_even
val t : QCheck.Test.t = QCheck2.Test.Test <abstr>

If we want to change the number of arbitraries that are checked, we can pass an optional integer argument ~count to QCheck.Test.make.
如果我们想更改检查的任意数量,我们可以将可选的整数参数 ~count 传递给 QCheck.Test.make

We can run that test with QCheck_runner.run_tests : QCheck.Test.t list -> int. (Once more, that function takes some optional arguments that we elide here.) The integer it returns is 0 if all the tests in the list pass, and 1 otherwise. For the test above, running it will output 1 with high probability, because it will generate at least one odd integer.
我们可以使用 QCheck_runner.run_tests : QCheck.Test.t list -> int 运行该测试。 (该函数再次采用一些我们在此处省略的可选参数。)如果列表中的所有测试都通过,则它返回的整数为 0,否则返回 1。对于上面的测试,运行它很有可能会输出 1,因为它会生成至少一个奇数。

QCheck_runner.run_tests [t]

random seed: 303467739
- : int = 1

Unfortunately, that output isn’t very informative; it doesn’t tell us what particular values failed to satisfy the property! We’ll fix that problem in a little while.
不幸的是,该输出的信息量并不大。它没有告诉我们哪些特定值无法满足该属性!我们稍后会解决这个问题。

If you want to make an OCaml program that runs QCheck tests and prints the results, there is a function QCheck_runner.run_tests_main that works much like OUnit2.run_test_tt_main: just invoke it as the final expression in a test file. For example:
如果您想创建一个运行 QCheck 测试并打印结果的 OCaml 程序,可以使用一个函数 QCheck_runner.run_tests_main ,其工作方式与 OUnit2.run_test_tt_main 非常相似:只需将其作为测试中的最终表达式调用即可文件。例如:

let tests = (* code that constructs a [QCheck.Test.t list] *)
let _ = QCheck_runner.run_tests_main tests

To compile QCheck code, just add the qcheck library to your dune file:
要编译 QCheck 代码,只需将 qcheck 库添加到 dune 文件中:

(executable
 ...
 (libraries ... qcheck))

QCheck tests can be converted to OUnit tests and included in the usual kind of OUnit test suite we’ve been writing all along. The function that does this is:
QCheck 测试可以转换为 OUnit 测试,并包含在我们一直在编写的通常类型的 OUnit 测试套件中。执行此操作的函数是:

QCheck_runner.to_ounit2_test
- : ?rand:Random.State.t -> QCheck2.Test.t -> OUnit2.test = <fun>

6.6.4. Informative Output from QCheck
6.6.4. QCheck 的信息输出 ¶

We noted above that the output of QCheck so far has told us only whether some arbitraries satisfied a property, but not which arbitraries failed to satisfy it. Let’s fix that problem.
我们在上面注意到,到目前为止 QCheck 的输出只告诉我们某些任意者是否满足某个属性,但不告诉我们哪些任意者未能满足该属性。让我们解决这个问题。

The issue is with how we constructed an arbitrary directly out of a generator. An arbitrary is properly more than just a generator. The QCheck library needs to know how to print values of the generator, and a few other things as well. You can see that in the definition of 'a QCheck.arbitrary:
问题在于我们如何直接从生成器构造任意值。任意的不仅仅是一个生成器。 QCheck 库需要知道如何打印生成器的值,以及其他一些事情。您可以在 'a QCheck.arbitrary 的定义中看到:

#show QCheck.arbitrary;;
type 'a arbitrary = private {
  gen : 'a QCheck.Gen.t;
  print : ('a -> string) option;
  small : ('a -> int) option;
  shrink : 'a QCheck.Shrink.t option;
  collect : ('a -> string) option;
  stats : 'a QCheck.stat list;
}

In addition to the generator field gen, there is a field containing an optional function to print values from the generator, and a few other optional fields as well. Luckily, we don’t usually have to find a way to complete those fields ourselves; the QCheck module provides many arbitraries that correspond to the generators found in QCheck.Gen:
除了生成器字段 gen 之外,还有一个包含用于打印生成器中的值的可选函数的字段,以及其他一些可选字段。幸运的是,我们通常不需要找到一种方法来自己完成这些领域;我们只需要做一些事情即可。 QCheck 模块提供了许多与 QCheck.Gen 中的生成器相对应的任意值:

module QCheck :
  sig
    ...
  val int : int arbitrary
  val small_int : int arbitrary
  val int_range : int -> int -> int arbitrary
  val list : 'a arbitrary -> 'a list arbitrary
  val list_of_size : int Gen.t -> 'a arbitrary -> 'a list arbitrary
  val string : string arbitrary
  val small_string : string arbitrary
    ...
  end

Using those arbitraries, we can get improved error messages:
使用这些任意值,我们可以获得改进的错误消息:

let t = QCheck.Test.make ~name:"my_test" QCheck.int is_even;;
QCheck_runner.run_tests [t];;
val t : QCheck.Test.t = QCheck2.Test.Test <abstr>

--- Failure --------------------------------------------------------------------

Test my_test failed (94 shrink steps):

3
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
- : int = 1

The output tells us the my_test failed, and shows us the input that caused the failure.
输出告诉我们 my_test 失败,并向我们显示导致失败的输入。

6.6.5. Testing Functions with QCheck
6.6.5. 用 QCheck 检验函数 ¶

The final piece of the QCheck puzzle is to use a randomly generated input to test whether a function’s output satisfies some property. For example, here is a QCheck test to see whether the output of double is correct:
QCheck 难题的最后一部分是使用随机生成的输入来测试函数的输出是否满足某些属性。例如,下面是一个 QCheck 测试,看看 double 的输出是否正确:

let double x = 2 * x;;
let double_check x = double x = x + x;;
let t = QCheck.Test.make ~count:1000 QCheck.int double_check;;
QCheck_runner.run_tests [t];;
val double : int -> int = <fun>
val double_check : int -> bool = <fun>
val t : QCheck.Test.t = QCheck2.Test.Test <abstr>
================================================================================
success (ran 1 tests)
- : int = 0

Above, double is the function we are testing. The property we’re testing double_check, is that double x is always x + x. We do that by having QCheck create 1000 arbitrary integers and test that the property holds of each of them.
上面的 double 是我们正在测试的函数。我们正在测试 double_check 的属性是 double x 始终为 x + x 。为此,我们让 QCheck 创建 1000 个任意整数并测试每个整数的属性是否成立。

Here are a couple more examples, drawn from QCheck’s own documentation. The first checks that List.rev is an involution, meaning that applying it twice brings you back to the original list. That is a property that should hold of a correct implementation of list reversal.
这里还有几个例子,取自 QCheck 自己的文档。第一个检查 List.rev 是否是对合,这意味着应用它两次会让您返回到原始列表。这是列表反转的正确实现应该具有的属性。

let rev_involutive lst = List.(lst |> rev |> rev = lst);;
let t = QCheck.(Test.make ~count:1000 (list int) rev_involutive);;
QCheck_runner.run_tests [t];;
val rev_involutive : 'a list -> bool = <fun>
val t : QCheck.Test.t = QCheck2.Test.Test <abstr>
================================================================================
success (ran 1 tests)
- : int = 0

Indeed, running 1000 random tests reveals that none of them fails. The int generator used above generates integers uniformly over the entire range of OCaml integers. The list generator creates lists whose elements are individual generated by int. According to the documentation of list, the length of each list is randomly generated by another generator nat, which generates “small natural numbers.” What does that mean? It isn’t specified. But if we read the current source code, we see that those are integers from 0 to 10,000, and biased toward being smaller numbers in that range.
事实上,运行 1000 次随机测试表明,没有一个失败。上面使用的 int 生成器在 OCaml 整数的整个范围内统一生成整数。 list 生成器创建列表,其元素由 int 单独生成。根据 list 的文档,每个列表的长度是由另一个生成器 nat 随机生成的,该生成器生成“小自然数”。这意味着什么?没有指定。但如果我们阅读当前的源代码,我们会发现这些是从 0 到 10,000 的整数,并且偏向于该范围内较小的数字。

The second example checks that all lists are sorted. Of course, not all lists are sorted! So we should expect this test to fail.
第二个示例检查所有列表是否已排序。当然,并非所有列表都已排序!所以我们应该预料到这个测试会失败。

let is_sorted lst = lst = List.sort Stdlib.compare lst;;
let t = QCheck.(Test.make ~count:1000 (list small_nat) is_sorted);;
QCheck_runner.run_tests [t];;
val is_sorted : 'a list -> bool = <fun>
val t : QCheck.Test.t = QCheck2.Test.Test <abstr>

--- Failure --------------------------------------------------------------------

Test anon_test_4 failed (7 shrink steps):

[1; 0]
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
- : int = 1

The output shows an example of a list that is not sorted, hence violates the property. Generator small_nat is like nat but ranges from 0 to 100.
输出显示了未排序的列表示例,因此违反了该属性。生成器 small_natnat 类似,但范围从 0 到 100。

6.7. Proving Correctness
6.7. 证明正确性 ¶

Testing provides evidence of correctness, but not full assurance. Even after extensive black-box and glass-box testing, maybe there’s still some test case the programmer failed to invent, and that test case would reveal a fault in the program.
测试提供正确性的证据,但不能完全保证。即使经过大量的黑盒和玻璃盒测试,也许仍然有一些测试用例程序员未能发明,并且该测试用例会揭示程序中的错误。

Program testing can be used to show the presence of bugs, but never to show their absence.
程序测试可用于显示错误的存在,但绝不能显示错误的不存在。

—Edsger W. Dijkstra ——埃兹格·W·迪杰斯特拉

The point is not that testing is useless! It can be quite effective. But it is a kind of inductive reasoning, in which evidence (i.e., passing tests) accumulates in support of a conclusion (i.e., correctness of the program) without absolutely guaranteeing the validity of that conclusion. (Note that the word “inductive” here is being used in a different sense than the proof technique known as induction.) To get that guarantee, we turn to deductive reasoning, in which we proceed from premises and rules about logic to a valid conclusion. In other words, we prove the correctness of the program. Our goal, next, is to learn some techniques for such correctness proofs. These techniques are known as formal methods because of their use of logical formalism.
重点不是测试没有用!它可能非常有效。但这是一种归纳推理,其中证据(即通过测试)的积累支持结论(即程序的正确性),但并不绝对保证该结论的有效性。 (请注意,这里使用的“归纳”一词与称为归纳的证明技术的含义不同。)为了获得这种保证,我们转向演绎推理,其中我们从有关逻辑的前提和规则出发,得出有效的结论。换句话说,我们证明了程序的正确性。接下来,我们的目标是学习一些用于此类正确性证明的技术。这些技术由于使用逻辑形式主义而被称为形式方法。

Correctness here means that the program produces the right output according to a specification. Specifications are usually provided in the documentation of a function (hence the name “specification comment”): they describe the program’s precondition and postcondition. Postconditions, as we have been writing them, have the form [f x] is "...a description of the output in terms of the input [x]...". For example, the specification of a factorial function could be:
这里的正确性意味着程序根据规范产生正确的输出。规范通常在函数的文档中提供(因此称为“规范注释”):它们描述了程序的前置条件和后置条件。正如我们所写的,后置条件的形式为 [f x] is "...a description of the output in terms of the input [x]..." 。例如,阶乘函数的规范可以是:

(** [fact n] is [n!]. Requires: [n >= 0]. *)
let rec fact n = ...

The postcondition is asserting an equality between the output of the function and some English description of a computation on the input. Formal verification is the task for proving that the implementation of the function satisfies its specification.
后置条件断言函数的输出与输入计算的一些英文描述之间相等。形式验证是证明功能的实现满足其规范的任务。

Equalities are one of the fundamental ways we think about correctness of functional programs. The absence of mutable state makes it possible to reason straightforwardly about whether two expressions are equal. It’s difficult to do that in an imperative language, because those expressions might have side effects that change the state.
等式是我们思考函数式程序正确性的基本方式之一。由于不存在可变状态,因此可以直接推断两个表达式是否相等。在命令式语言中很难做到这一点,因为这些表达式可能会产生改变状态的副作用。

6.7.1. Equality 6.7.1. 平等 ¶

When are two expressions equal? Two possible answers are:
什么时候两个表达式相等?两个可能的答案是:

  • When they are syntactically identical.
    当它们在语法上相同时。

  • When they are semantically equivalent: they produce the same value.
    当它们在语义上等效时:它们产生相同的值。

For example, are 42 and 41+1 equal? The syntactic answer would say they are not, because they involve different tokens. The semantic answer would say they are: they both produce the value 42.
例如, 4241+1 相等吗?语法答案会说它们不是,因为它们涉及不同的标记。语义答案会说它们是:它们都产生值 42

What about functions: are fun x -> x and fun y -> y equal? Syntactically they are different. But semantically, they both produce a value that is the identity function: when they are applied to an input, they will both produce the same output. That is, (fun x -> x) z = z, and (fun y -> y) z = z. If it is the case that for all inputs two functions produce the same output, we will consider the functions to be equal:
函数呢: fun x -> xfun y -> y 相等吗?从语法上来说,它们是不同的。但从语义上讲,它们都产生一个作为恒等函数的值:当它们应用于输入时,它们都会产生相同的输出。即 (fun x -> x) z = z(fun y -> y) z = z 。如果对于所有输入,两个函数产生相同的输出,我们将认为这些函数是相等的:

if (forall x, f x = g x), then f = g.

That definition of equality for functions is known as the Axiom of Extensionality in some branches of mathematics; henceforth we’ll refer to it simply as “extensionality”.
函数等式的定义在数学的某些分支中被称为外延公理。从今以后,我们将其简称为“外延性”。

Here we will adopt the semantic approach. If e1 and e2 evaluate to the same value v, then we write e1 = e2. We are using = here in a mathematical sense of equality, not as the OCaml polymorphic equality operator. For example, we allow (fun x -> x) = (fun y -> y), even though OCaml’s operator would raise an exception and refuse to compare functions.
这里我们将采用语义方法。如果 e1e2 计算结果为相同的值 v ,则我们编写 e1 = e2 。我们在这里使用 = 是在数学意义上的相等,而不是 OCaml 多态相等运算符。例如,我们允许 (fun x -> x) = (fun y -> y) ,即使 OCaml 的运算符会引发异常并拒绝比较函数。

We’re also going to restrict ourselves to expressions that are well typed, pure (meaning they have no side effects), and total (meaning they don’t have exceptions or infinite loops).
我们还将把自己限制在类型良好、纯粹(意味着它们没有副作用)和总体(意味着它们没有异常或无限循环)的表达式上。

6.7.2. Equational Reasoning
6.7.2. 等式推理 ¶

Consider these functions:
考虑这些函数:

let twice f x = f (f x)
let compose f g x = f (g x)
val twice : ('a -> 'a) -> 'a -> 'a = <fun>
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

We know from the rules of OCaml evaluation that twice h x = h (h x), and likewise, compose h h x = h (h x). Thus we have:
我们从 OCaml 评估规则知道 twice h x = h (h x) ,同样, compose h h x = h (h x) 。因此我们有:

twice h x = h (h x) = compose h h x

Therefore can conclude that twice h x = compose h h x. And by extensionality we can simplify that equality: Since twice h x = compose h h x holds for all x, we can conclude twice h = compose h h.
因此可以得出 twice h x = compose h h x 。通过外延性,我们可以简化这种等式:由于 twice h x = compose h h x 对于所有 x 都成立,因此我们可以得出 twice h = compose h h 的结论。

As another example, suppose we define an infix operator for function composition:
再举个例子,假设我们为函数组合定义了一个中缀运算符:

let ( << ) = compose

Then we can prove that composition is associative, using equational reasoning:
然后我们可以使用等式推理来证明组合是关联的:

Theorem: (f << g) << h  =  f << (g << h)

Proof: By extensionality, we need to show
  ((f << g) << h) x  =  (f << (g << h)) x
for an arbitrary x.

  ((f << g) << h) x
= (f << g) (h x)
= f (g (h x))

and

  (f << (g << h)) x
= f ((g << h) x)
= f (g (h x))

So ((f << g) << h) x = f (g (h x)) = (f << (g << h)) x.

QED

All of the steps in the equational proof above follow from evaluation. Another format for writing the proof would provide hints as to why each step is valid:
上述等式证明中的所有步骤均来自评估。另一种编写证明的格式将提供有关为什么每个步骤有效的提示:

  ((f << g) << h) x
=   { evaluation of << }
  (f << g) (h x)
=   { evaluation of << }
  f (g (h x))

and

  (f << (g << h)) x
=   { evaluation of << }
  f ((g << h) x)
=   { evaluation of << }
  f (g (h x))

6.7.3. Induction on Natural Numbers
6.7.3. 自然数归纳法 ¶

The following function sums the non-negative integers up to n:
以下函数对 n 以内的非负整数求和:

let rec sumto n =
  if n = 0 then 0 else n + sumto (n - 1)
val sumto : int -> int = <fun>

You might recall that the same summation can be expressed in closed form as n * (n + 1) / 2. To prove that forall n >= 0, sumto n = n * (n + 1) / 2, we will need mathematical induction.
您可能还记得,相同的求和可以以闭合形式表示为 n * (n + 1) / 2 。为了证明 forall n >= 0, sumto n = n * (n + 1) / 2 ,我们需要数学归纳法。

Recall that induction on the natural numbers (i.e., the non-negative integers) is formulated as follows:
回想一下,自然数(即非负整数)的归纳法公式如下:

forall properties P,
  if P(0),
  and if forall k, P(k) implies P(k + 1),
  then forall n, P(n)

That is called the induction principle for natural numbers. The base case is to prove P(0), and the inductive case is to prove that P(k + 1) holds under the assumption of the inductive hypothesis P(k).
这就是所谓的自然数归纳原理。基本情况是证明 P(0) ,归纳情况是证明 P(k + 1) 在归纳假设 P(k) 的假设下成立。

Let’s use induction to prove the correctness of sumto.
我们用归纳法来证明 sumto 的正确性。

Claim: sumto n = n * (n + 1) / 2

Proof: by induction on n.
P(n) = sumto n = n * (n + 1) / 2

Base case: n = 0
Show: sumto 0 = 0 * (0 + 1) / 2

  sumto 0
=   { evaluation }
  0
=   { algebra }
  0 * (0 + 1) / 2

Inductive case: n = k + 1
Show: sumto (k + 1) = (k + 1) * ((k + 1) + 1) / 2
IH: sumto k = k * (k + 1) / 2

  sumto (k + 1)
=   { evaluation }
  k + 1 + sumto k
=   { IH }
  k + 1 + k * (k + 1) / 2
=   { algebra }
  (k + 1) * (k + 2) / 2

QED

Note that we have been careful in each of the cases to write out what we need to show, as well as to write down the inductive hypothesis. It is important to show all this work.
请注意,我们在每种情况下都非常小心地写出我们需要展示的内容,以及写下归纳假设。展示所有这些作品很重要。

Suppose we now define:
假设我们现在定义:

let sumto_closed n = n * (n + 1) / 2
val sumto_closed : int -> int = <fun>

Then as a corollary to our previous claim, by extensionality we can conclude
那么作为我们之前的主张的推论,通过外延性我们可以得出结论

sumto_closed = sumto

Technically that equality holds only inputs that are natural numbers. But since all our examples henceforth will be for naturals, not integers per se, we will elide stating any preconditions or restrictions regarding natural numbers.
从技术上讲,等式仅包含自然数输入。但是,由于今后我们所有的示例都将针对自然数,而不是整数本身,因此我们将省略说明有关自然数的任何先决条件或限制。

6.7.4. Programs as Specifications
6.7.4. 程序作为规范 ¶

We have just proved the correctness of an efficient implementation relative to an inefficient implementation. The inefficient implementation, sumto, serves as a specification for the efficient implementation, sumto_closed.
我们刚刚证明了高效实现相对于低效实现的正确性。低效实现 sumto 充当高效实现 sumto_closed 的规范。

That technique is common in verifying functional programs: write an obviously correct implementation that is lacking in some desired property, such as efficiency, then prove that a better implementation is equal to the original.
这种技术在验证功能性程序中很常见:编写一个明显正确的实现,但缺乏某些所需的属性(例如效率),然后证明更好的实现与原始实现相同。

Let’s do another example of this kind of verification. This time, well use the factorial function.
让我们再举一个此类验证的例子。这次我们就使用阶乘函数。

The simple, obviously correct implementation of factorial would be:
阶乘的简单且明显正确的实现是:

let rec fact n =
  if n = 0 then 1 else n * fact (n - 1)
val fact : int -> int = <fun>

A tail-recursive implementation would be more efficient about stack space:
尾递归实现对于堆栈空间会更有效:

let rec facti acc n =
  if n = 0 then acc else facti (acc * n) (n - 1)

let fact_tr n = facti 1 n
val facti : int -> int -> int = <fun>
val fact_tr : int -> int = <fun>

The i in the name facti stands for iterative. We call this an iterative implementation because it strongly resembles how the same computation would be expressed using a loop (that is, an iteration construct) in an imperative language. For example, in Java we might write:
名称 facti 中的 i 代表迭代。我们将其称为迭代实现,因为它非常类似于使用命令式语言中的循环(即迭代构造)来表达相同计算的方式。例如,在 Java 中我们可以这样写:

int facti (int n) {
  int acc = 1;
  while (n != 0) {
    acc *= n;
    n--;
  }
  return acc;
}

Both the OCaml and Java implementation of facti share these features:
facti 的 OCaml 和 Java 实现共享以下功能:

  • they start acc at 1
    他们从 acc 开始于 1

  • they check whether n is 0
    他们检查 n 是否为 0

  • they multiply acc by n
    他们将 acc 乘以 n

  • they decrement n
    他们减少 n

  • they return the accumulator, acc
    他们返回累加器, acc

Let’s try to prove that fact_tr correctly implements the same computation as fact.
让我们尝试证明 fact_tr 正确地实现了与 fact 相同的计算。

Claim: forall n, fact n = fact_tr n

Since fact_tr n = facti 1 n, it suffices to show fact n = facti 1 n.

Proof: by induction on n.
P(n) = fact n = facti 1 n

Base case: n = 0
Show: fact 0 = facti 1 0

  fact 0
=   { evaluation }
  1
=   { evaluation }
  facti 1 0

Inductive case: n = k + 1
Show: fact (k + 1) = facti 1 (k + 1)
IH: fact k = facti 1 k

  fact (k + 1)
=   { evaluation }
  (k + 1) * fact k
=   { IH }
  (k + 1) * facti 1 k

  facti 1 (k + 1)
=   { evaluation }
  facti (1 * (k + 1)) k
=   { evaluation }
  facti (k + 1) k

Unfortunately, we're stuck.  Neither side of what we want to show
can be manipulated any further.

ABORT

We know that facti (k + 1) k and (k + 1) * facti 1 k should yield the same value. But the IH allows us only to use 1 as the second argument to facti, instead of a bigger argument like k + 1. So our proof went astray the moment we used the IH. We need a stronger inductive hypothesis!
我们知道 facti (k + 1) k(k + 1) * facti 1 k 应该产生相同的值。但 IH 允许我们仅使用 1 作为 facti 的第二个参数,而不是像 k + 1 这样的更大的参数。因此,当我们使用 IH 时,我们的证明就误入歧途了。我们需要一个更强的归纳假设!

So let’s strengthen the claim we are making. Instead of showing that fact n = facti 1 n, we’ll try to show forall p, p * fact n = facti p n. That generalizes the k + 1 we were stuck on to an arbitrary quantity p.
因此,让我们加强我们的主张。我们不会显示 fact n = facti 1 n ,而是尝试显示 forall p, p * fact n = facti p n 。这概括了我们坚持任意数量 pk + 1

Claim: forall n, forall p . p * fact n = facti p n

Proof: by induction on n.
P(n) = forall p, p * fact n = facti p n

Base case:  n = 0
Show: forall p,  p * fact 0 = facti p 0

  p * fact 0
=   { evaluation and algebra }
  p
=   { evaluation }
  facti p 0

Inductive case: n = k + 1
Show: forall p,  p * fact (k + 1) = facti p (k + 1)
IH: forall p,  p * fact k = facti p k

  p * fact (k + 1)
=   { evaluation }
  p * (k + 1) * fact k
=   { IH, instantiating its p as p * (k + 1) }
  facti (p * (k + 1)) k

  facti p (k + 1)
=   { evaluation }
  facti (p * (k + 1)) k

QED

Claim: forall n, fact n = fact_tr n

Proof:

  fact n
=   { algebra }
  1 * fact n
=   { previous claim }
  facti 1 n
=   { evaluation }
  fact_tr n

QED

That finishes our proof that the efficient, tail-recursive function fact_tr is equivalent to the simple, recursive function fact. In essence, we have proved the correctness of fact_tr using fact as its specification.
这就完成了我们的证明,即高效的尾递归函数 fact_tr 相当于简单的递归函数 fact 。本质上,我们已经证明了使用 fact 作为其规范的 fact_tr 的正确性。

6.7.5. Recursion vs. Iteration
6.7.5. 递归与迭代 ¶

We added an accumulator as an extra argument to make the factorial function be tail recursive. That’s a trick we’ve seen before. Let’s abstract and see how to do it in general.
我们添加了一个累加器作为额外参数,以使阶乘函数成为尾递归。这是我们以前见过的伎俩。让我们抽象一下,看看一般如何做。

Suppose we have a recursive function over integers:
假设我们有一个整数的递归函数:

let rec f_r n =
  if n = 0 then i else op n (f_r (n - 1))

Here, the r in f_r is meant to suggest that f_r is a recursive function. The i and op are pieces of the function that are meant to be replaced by some concrete value i and operator op. For example, with the factorial function, we have:
这里, f_r 中的 r 意味着 f_r 是一个递归函数。 iop 是函数的一部分,旨在被某些具体值 i 和运算符 op 替换。例如,对于阶乘函数,我们有:

f_r = fact
i = 1
op = ( * )

Such a function can be made tail recursive by rewriting it as follows:
这样的函数可以通过如下重写来使其成为尾递归:

let rec f_i acc n =
  if n = 0 then acc
  else f_i (op acc n) (n - 1)

let f_tr = f_i i

Here, the i in f_i is meant to suggest that f_i is an iterative function, and i and op are the same as in the recursive version of the function. For example, with factorial we have:
这里, f_i 中的 i 意味着 f_i 是一个迭代函数,而 iop 与函数的递归版本中的相同。例如,对于阶乘,我们有:

f_i = fact_i
i = 1
op = ( * )
f_tr = fact_tr

We can prove that f_r and f_tr compute the same function. During the proof, next, we will discover certain conditions that must hold of i and op to make the transformation to tail recursion be correct.
我们可以证明 f_rf_tr 计算相同的函数。接下来在证明过程中,我们将发现 iop 必须满足某些条件才能使尾递归的转换正确。

Theorem: f_r = f_tr

Proof:  By extensionality, it suffices to show that forall n, f_r n = f_tr n.

As in the previous proof for fact, we will need a strengthened induction
hypothesis. So we first prove this lemma, which quantifies over all accumulators
that could be input to f_i, rather than only i:

  Lemma: forall n, forall acc, op acc (f_r n) = f_i acc n

  Proof of Lemma: by induction on n.
  P(n) = forall acc, op acc (f_r n) = f_i acc n

  Base: n = 0
  Show: forall acc, op acc (f_r 0) = f_i acc 0

    op acc (f_r 0)
  =   { evaluation }
    op acc i
  =   { if we assume forall x, op x i = x }
    acc

    f_i acc 0
  =   { evaluation }
    acc

  Inductive case: n = k + 1
  Show: forall acc, op acc (f_r (k + 1)) = f_i acc (k + 1)
  IH: forall acc, op acc (f_r k) = f_i acc k

    op acc (f_r (k + 1))
  =   { evaluation }
    op acc (op (k + 1) (f_r k))
  =   { if we assume forall x y z, op x (op y z) = op (op x y) z }
    op (op acc (k + 1)) (f_r k)

    f_i acc (k + 1)
  =   { evaluation }
    f_i (op acc (k + 1)) k
  =   { IH, instantiating acc as op acc (k + 1)}
    op (op acc (k + 1)) (f_r k)

  QED

The proof then follows almost immediately from the lemma:

  f_r n
=   { if we assume forall x, op i x = x }
  op i (f_r n)
=   { lemma, instantiating acc as i }
  f_i i n
=   { evaluation }
  f_tr n

QED

Along the way we made three assumptions about i and op:
在此过程中,我们对 i 和 op 做出了三个假设:

  1. forall x, op x i = x

  2. op x (op y z) = op (op x y) z

  3. forall x, op i x = x

The first and third say that i is an identity of op: using it on the left or right side leaves the other argument x unchanged. The second says that op is associative. Both those assumptions held for the values we used in the factorial functions:
第一个和第三个表示 iop 的恒等式:在左侧或右侧使用它会使另一个参数 x 保持不变。第二个表示 op 是关联的。这两个假设都适用于我们在阶乘函数中使用的值:

  • op is multiplication, which is associative.
    op 是乘法,具有结合律。

  • i is 1, which is an identity of multiplication: multiplication by 1 leaves the other argument unchanged.
    i1 ,它是乘法的恒等式:乘以 1 使另一个参数保持不变。

So our transformation from a recursive to a tail-recursive function is valid as long as the operator applied in the recursive call is associative, and the value returned in the base case is an identity of that operator.
因此,只要递归调用中应用的运算符是关联的,并且基本情况中返回的值是该运算符的标识,我们从递归函数到尾递归函数的转换就是有效的。

Returning to the sumto function, we can apply the theorem we just proved to immediately get a tail-recursive version:
回到 sumto 函数,我们可以应用刚刚证明的定理立即得到尾递归版本:

let rec sumto_r n =
  if n = 0 then 0 else n + sumto_r (n - 1)
val sumto_r : int -> int = <fun>

Here, the operator is addition, which is associative; and the base case is zero, which is an identity of addition. Therefore our theorem applies, and we can use it to produce the tail-recursive version without even having to think about it:
这里,运算符是加法,具有结合性;基本情况为零,这是加法的恒等式。因此,我们的定理适用,我们可以使用它来生成尾递归版本,甚至无需考虑它:

let rec sumto_i acc n =
  if n = 0 then acc else sumto_i (acc + n) (n - 1)

let sumto_tr = sumto_i 0
val sumto_i : int -> int -> int = <fun>
val sumto_tr : int -> int = <fun>

We already know that sumto_tr is correct, thanks to our theorem.
由于我们的定理,我们已经知道 sumto_tr 是正确的。

6.7.6. Termination 6.7.6. 终止 ¶

Sometimes correctness of programs is further divided into:
有时程序的正确性进一步分为:

  • partial correctness: meaning that if a program terminates, then its output is correct; and
    部分正确性:意味着如果程序终止,则其输出是正确的;和

  • total correctness: meaning that a program does terminate, and its output is correct.
    完全正确性:意味着程序确实终止,并且其输出是正确的。

Total correctness is therefore the conjunction of partial correctness and termination. Thus far, we have been proving partial correctness.
因此,完全正确性是部分正确性和终止的结合。到目前为止,我们已经证明了部分正确性。

To prove that a program terminates is difficult. Indeed, it is impossible in general for an algorithm to do so: a computer can’t precisely decide whether a program will terminate. (Look up the “halting problem” for more details.) But, a smart human sometimes can do so.
证明程序终止是很困难的。事实上,一般来说算法不可能做到这一点:计算机无法精确地决定程序是否会终止。 (有关更多详细信息,请参阅“停机问题”。)但是,聪明的人有时可以做到这一点。

There is a simple heuristic that can be used to show that a recursive function terminates:
有一个简单的启发式方法可以用来表明递归函数终止:

  • All recursive calls are on a “smaller” input, and
    所有递归调用都在“较小”的输入上,并且

  • all base cases are terminating.
    所有基本情况都将终止。

For example, consider the factorial function:
例如,考虑阶乘函数:

let rec fact n =
  if n = 0 then 1
  else n * fact (n - 1)
val fact : int -> int = <fun>

The base case, 1, obviously terminates. The recursive call is on n - 1, which is a smaller input than the original n. So fact always terminates (as long as its input is a natural number).
基本情况 1 显然已经终止。递归调用位于 n - 1 上,该输入比原始 n 更小。所以 fact 总是终止(只要它的输入是自然数)。

The same reasoning applies to all the other functions we’ve discussed above.
同样的推理也适用于我们上面讨论的所有其他函数。

To make this more precise, we need a notion of what it means to be smaller. Suppose we have a binary relation < on inputs. Despite the notation, this relation need not be the less-than relation on integers—although that will work for fact. Also suppose that it is never possible to create an infinite sequence x0 > x1 > x2 > x3 ... of elements using this relation. (Where of course a > b iff b < a.) That is, there are no infinite descending chains of elements: once you pick a starting element x0, there can be only a finite number of “descents” according to the < relation before you bottom out and hit a base case. This property of < makes it a well-founded relation.
为了使这一点更加精确,我们需要了解“变小”意味着什么。假设我们在输入上有一个二元关系 < 。尽管有这种表示法,但这种关系不一定是整数上的小于关系,尽管这适用于 fact 。还假设永远不可能使用此关系创建元素的无限序列 x0 > x1 > x2 > x3 ... 。 (当然 a > b iff b < a 。)也就是说,不存在无限下降的元素链:一旦选择起始元素 x0 ,就可以有在触底并达到基本情况之前,根据 < 关系,只有有限数量的“下降”。 < 的这一属性使其成为一种有充分依据的关系。

So, a recursive function terminates if all its recursive calls are on elements that are smaller according to <. Why? Because there can be only a finite number of calls before a base case is reached, and base cases must terminate.
因此,如果递归函数的所有递归调用都针对小于 < 的元素,则递归函数将终止。为什么?因为在达到基本情况之前只能进行有限数量的调用,并且基本情况必须终止。

The usual < relation is well-founded on the natural numbers, because eventually any chain must reach the base case of 0. But it is not well-founded on the integers, which can get just keep getting smaller: -1 > -2 > -3 > ....
通常的 < 关系在自然数上是有充分根据的,因为最终任何链都必须达到 0 的基本情况。但它在整数上却没有充分根据,整数可能会变得越来越小: -1 > -2 > -3 > ...

Here’s an interesting function for which the usual < relation doesn’t suffice to prove termination:
这是一个有趣的函数,通常的 < 关系不足以证明终止:

let rec ack = function
  | (0, n) -> n + 1
  | (m, 0) -> ack (m - 1, 1)
  | (m, n) -> ack (m - 1, ack (m, n - 1))
val ack : int * int -> int = <fun>

This is known as Ackermann’s function. It grows faster than any exponential function. Try running ack (1, 1), ack (2, 1), ack (3, 1), then ack (4, 1) to get a sense of that. It also is a famous example of a function that can be implemented with while loops but not with for loops. Nonetheless, it does terminate.
这就是所谓的阿克曼函数。它的增长速度比任何指数函数都快。尝试运行 ack (1, 1)ack (2, 1)ack (3, 1) ,然后运行 ​​ ack (4, 1) 来了解一下。它也是一个著名的函数示例,可以使用 while 循环实现,但不能使用 for 循环实现。尽管如此,它确实终止了。

To show that, the base case is easy: when the input is (0, _), the function terminates. But in other cases, it makes a recursive call, and we need to define an appropriate < relation. It turns out lexicographic ordering on pairs works. Define (a, b) < (c, d) if:
为了表明这一点,基本情况很简单:当输入为 (0, _) 时,函数终止。但在其他情况下,它会进行递归调用,我们需要定义适当的 < 关系。事实证明,成对作品的字典顺序是有效的。定义 (a, b) < (c, d) 如果:

  • a < c, or
    a < c ,或

  • a = c and b < d.
    a = cb < d

The < order in those two cases is the usual < on natural numbers.
这两种情况下的 < 顺序是自然数上常见的 < 顺序。

In the first recursive call, (m - 1, 1) < (m, 0) by the first case of the definition of <, because m - 1 < m. In the nested recursive call ack (m - 1, ack (m, n - 1)), both cases are needed:
在第一次递归调用中, (m - 1, 1) < (m, 0) 由第一种情况定义 < ,因为 m - 1 < m 。在嵌套递归调用 ack (m - 1, ack (m, n - 1)) 中,两种情况都需要:

  • (m, n - 1) < (m, n) because m = m and n - 1 < n
    (m, n - 1) < (m, n) 因为 m = mn - 1 < n

  • (m - 1, _) < (m, n) because m - 1 < m.
    (m - 1, _) < (m, n) 因为 m - 1 < m

6.8. Structural Induction
6.8. 结构归纳法 ¶

So far we’ve proved the correctness of recursive functions on natural numbers. We can do correctness proofs about recursive functions on variant types, too. That requires us to figure out how induction works on variants. We’ll do that, next, starting with a variant type for representing natural numbers, then generalizing to lists, trees, and other variants. This inductive proof technique is sometimes known as structural induction instead of mathematical induction. But that’s just a piece of vocabulary; don’t get hung up on it. The core idea is completely the same.
到目前为止,我们已经证明了自然数递归函数的正确性。我们也可以对变体类型上的递归函数进行正确性证明。这需要我们弄清楚归纳法如何作用于变体。接下来,我们将从表示自然数的变体类型开始,然后推广到列表、树和其他变体。这种归纳证明技术有时被称为结构归纳法而不是数学归纳法。但这只是一个词汇而已。不要沉迷于此。核心思想是完全一样的。

6.8.1. Induction on Naturals
6.8.1. 自然归纳法 ¶

We used OCaml’s int type as a representation of the naturals. Of course, that type is somewhat of a mismatch: negative int values don’t represent naturals, and there is an upper bound to what natural numbers we can represent with int.
我们使用 OCaml 的 int 类型作为自然数的表示。当然,这种类型有点不匹配:负 int 值不代表自然数,并且我们可以用 int 表示的自然数有一个上限。

Let’s fix those problems by defining our own variant to represent natural numbers:
让我们通过定义自己的变体来表示自然数来解决这些问题:

type nat = Z | S of nat
type nat = Z | S of nat

The constructor Z represents zero; and the constructor S represents the successor of another natural number. So,
构造函数 Z 代表零;构造函数 S 代表另一个自然数的后继。所以,

  • 0 is represented by Z,
    0 用 Z 表示,

  • 1 by S Z, 1 用 S Z

  • 2 by S (S Z), 2 用 S (S Z)

  • 3 by S (S (S Z)), 3 用 S (S (S Z))

and so forth. This variant is thus a unary (as opposed to binary or decimal) representation of the natural numbers: the number of times S occurs in a value n : nat is the natural number that n represents.
等等。因此,此变体是自然数的一元(与二进制或十进制相对)表示:值 n : nat 中出现 S 的次数是 n 代表。

We can define addition on natural numbers with the following function:
我们可以使用以下函数定义自然数的加法:

let rec plus a b =
  match a with
  | Z -> b
  | S k -> S (plus k b)
val plus : nat -> nat -> nat = <fun>

Immediately we can prove the following rather trivial claim:
我们可以立即证明以下相当微不足道的主张:

Claim:  plus Z n = n

Proof:

  plus Z n
=   { evaluation }
  n

QED

But suppose we want to prove this also trivial-seeming claim:
但假设我们想证明这个看似微不足道的主张:

Claim:  plus n Z = n

Proof:

  plus n Z
=
  ???

We can’t just evaluate plus n Z, because plus matches against its first argument, not second. One possibility would be to do a case analysis: what if n is Z, vs. S k for some k? Let’s attempt that.
我们不能只评估 plus n Z ,因为 plus 匹配它的第一个参数,而不是第二个参数。一种可能是进行案例分析:如果 nZ ,而 S k 对于某些 k 又如何?让我们尝试一下。

Proof:

By case analysis on n, which must be either Z or S k.

Case:  n = Z

  plus Z Z
=   { evaluation }
  Z

Case:  n = S k

  plus (S k) Z
=   { evaluation }
  S (plus k Z)
=
  ???

We are again stuck, and for the same reason: once more plus can’t be evaluated any further.
我们再次陷入困境,并且出于同样的原因:再次无法进一步评估 plus

When you find yourself needing to solve the same subproblem in programming, you use recursion. When it happens in a proof, you use induction!
当您发现自己需要在编程中解决相同的子问题时,您可以使用递归。当它发生在证明中时,你可以使用归纳法!

We’ll need an induction principle for nat. Here it is:
我们需要 nat 的归纳原理。这里是:

forall properties P,
  if P(Z),
  and if forall k, P(k) implies P(S k),
  then forall n, P(n)

Compare that to the induction principle we used for natural numbers before, when we were using int in place of natural numbers:
与我们之前用于自然数的归纳原理进行比较,当时我们使用 int 代替自然数:

forall properties P,
  if P(0),
  and if forall k, P(k) implies P(k + 1),
  then forall n, P(n)

There’s no essential difference between the two: we just use Z in place of 0, and S k in place of k + 1.
两者没有本质区别:我们只是用 Z 代替 0 ,用 S k 代替 k + 1

Using that induction principle, we can carry out the proof:
利用归纳原理,我们可以进行证明:

Claim:  plus n Z = n

Proof: by induction on n.
P(n) = plus n Z = n

Base case: n = Z
Show: plus Z Z = Z

  plus Z Z
=   { evaluation }
  Z

Inductive case: n = S k
IH: plus k Z = k
Show: plus (S k) Z = S k

  plus (S k) Z
=   { evaluation }
  S (plus k Z)
=   { IH }
  S k

QED

6.8.2. Induction on Lists
6.8.2. 列表归纳 ¶

It turns out that natural numbers and lists are quite similar, when viewed as data types. Here are the definitions of both, aligned for comparison:
事实证明,当将自然数和列表视为数据类型时,它们非常相似。以下是两者的定义,为了比较而对齐:

type    nat  = Z  | S      of nat
type 'a list = [] | ( :: ) of 'a * 'a list

Both types have a constructor representing a concept of “nothing”. Both types also have a constructor representing “one more” than another value of the type: S n is one more than n, and h :: t is a list with one more element than t.
两种类型都有一个代表“无”概念的构造函数。这两种类型都有一个构造函数,表示比该类型的另一个值“多一个”: S nn 多一个,而 h :: t 是一个列表,其中一个比 t 更多的元素。

The induction principle for lists is likewise quite similar to the induction principle for natural numbers. Here is the principle for lists:
列表的归纳原理同样与自然数的归纳原理非常相似。列表的原理如下:

forall properties P,
  if P([]),
  and if forall h t, P(t) implies P(h :: t),
  then forall lst, P(lst)

An inductive proof for lists therefore has the following structure:
因此,列表的归纳证明具有以下结构:

Proof: by induction on lst.
P(lst) = ...

Base case: lst = []
Show: P([])

Inductive case: lst = h :: t
IH: P(t)
Show: P(h :: t)

Let’s try an example of this kind of proof. Recall the definition of the append operator:
让我们尝试举一个此类证明的例子。回想一下追加运算符的定义:

let rec append lst1 lst2 =
  match lst1 with
  | [] -> lst2
  | h :: t -> h :: append t lst2

let ( @ ) = append
val append : 'a list -> 'a list -> 'a list = <fun>
val ( @ ) : 'a list -> 'a list -> 'a list = <fun>

We’ll prove that append is associative.
我们将证明append是关联的。

Theorem: forall xs ys zs, xs @ (ys @ zs) = (xs @ ys) @ zs

Proof: by induction on xs.
P(xs) = forall ys zs, xs @ (ys @ zs) = (xs @ ys) @ zs

Base case: xs = []
Show: forall ys zs, [] @ (ys @ zs) = ([] @ ys) @ zs

  [] @ (ys @ zs)
=   { evaluation }
  ys @ zs
=   { evaluation }
  ([] @ ys) @ zs

Inductive case: xs = h :: t
IH: forall ys zs, t @ (ys @ zs) = (t @ ys) @ zs
Show: forall ys zs, (h :: t) @ (ys @ zs) = ((h :: t) @ ys) @ zs

  (h :: t) @ (ys @ zs)
=   { evaluation }
  h :: (t @ (ys @ zs))
=   { IH }
  h :: ((t @ ys) @ zs)

  ((h :: t) @ ys) @ zs
=   { evaluation of inner @ }
  (h :: (t @ ys)) @ zs
=   { evaluation of outer @ }
  h :: ((t @ ys) @ zs)

QED

6.8.3. A Theorem about Folding
6.8.3. 关于折叠的定理 ¶

When we studied List.fold_left and List.fold_right, we discussed how they sometimes compute the same function, but in general do not. For example,
当我们研究 List.fold_leftList.fold_right 时,我们讨论了它们有时如何计算相同的函数,但通常不会。例如,

  List.fold_left ( + ) 0 [1; 2; 3]
= (((0 + 1) + 2) + 3
= 6
= 1 + (2 + (3 + 0))
= List.fold_right ( + ) lst [1; 2; 3]

but

  List.fold_left ( - ) 0 [1; 2; 3]
= (((0 - 1) - 2) - 3
= -6
<> 2
= 1 - (2 - (3 - 0))
= List.fold_right ( - ) lst [1; 2; 3]

Based on the equations above, it looks like the fact that + is commutative and associative, whereas - is not, explains this difference between when the two fold functions get the same answer. Let’s prove it!
根据上面的方程,看起来 + 是可交换和结合的,而 - 不是,这解释了两个折叠函数得到相同答案时的差异。让我们证明一下!

First, recall the definitions of the fold functions:
首先,回顾一下折叠函数的定义:

let rec fold_left f acc lst =
  match lst with
  | [] -> acc
  | h :: t -> fold_left f (f acc h) t

let rec fold_right f lst acc =
  match lst with
  | [] -> acc
  | h :: t -> f h (fold_right f t acc)
val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun>

Second, recall what it means for a function f : 'a -> 'a to be commutative and associative:
其次,回想一下函数 f : 'a -> 'a 的可交换性和结合性意味着什么:

Commutative:  forall x y, f x y = f y x
Associative:  forall x y z, f x (f y z) = f (f x y) z

Those might look a little different than the normal formulations of those properties, because we are using f as a prefix operator. If we were to write f instead as an infix operator op, they would look more familiar:
这些看起来可能与这些属性的正常表述有些不同,因为我们使用 f 作为前缀运算符。如果我们将 f 写成中缀运算符 op ,它们看起来会更熟悉:

Commutative:  forall x y, x op y = y op x
Associative:  forall x y z, x op (y op z) = (x op y) op z

When f is both commutative and associative we have this little interchange lemma that lets us swap two arguments around:
f 既可交换又可结合时,我们有这个小交换引理,可以让我们交换两个参数:

Lemma (interchange): f x (f y z) = f y (f x z)

Proof:

  f x (f y z)
=   { associativity }
  f (f x y) z
=   { commutativity }
  f (f y x) z
=   { associativity }
  f y (f z x)

QED

Now we’re ready to state and prove the theorem.
现在我们准备陈述并证明该定理。

Theorem: If f is commutative and associative, then
  forall lst acc,
    fold_left f acc lst = fold_right f lst acc.

Proof: by induction on lst.
P(lst) = forall acc,
  fold_left f acc lst = fold_right f lst acc

Base case: lst = []
Show: forall acc,
  fold_left f acc [] = fold_right f [] acc

  fold_left f acc []
=   { evaluation }
  acc
=   { evaluation }
  fold_right f [] acc

Inductive case: lst = h :: t
IH: forall acc,
  fold_left f acc t = fold_right f t acc
Show: forall acc,
  fold_left f acc (h :: t) = fold_right f (h :: t) acc

  fold_left f acc (h :: t)
=   { evaluation }
  fold_left f (f acc h) t
=   { IH with acc := f acc h }
  fold_right f t (f acc h)

  fold_right f (h :: t) acc
=   { evaluation }
  f h (fold_right f t acc)

Now, it might seem as though we are stuck: the left and right sides of the equality we want to show have failed to “meet in the middle.” But we’re actually in a similar situation to when we proved the correctness of facti earlier: there’s something (applying f to h and another argument) that we want to push into the accumulator of that last line (so that we have f acc h).
现在,我们似乎陷入了困境:我们想要展示的平等的左侧和右侧未能“在中间相遇”。但我们实际上处于与之前证明 facti 正确性时类似的情况:有一些东西(将 f 应用于 h 和另一个参数)我们想要推入最后一行的累加器(这样我们就有 f acc h )。

Let’s try proving that with its own lemma:
让我们尝试用它自己的引理来证明这一点:

Lemma: forall lst acc x,
  f x (fold_right f lst acc) = fold_right f lst (f acc x)

Proof: by induction on lst.
P(lst) = forall acc x,
  f x (fold_right f lst acc) = fold_right f lst (f acc x)

Base case: lst = []
Show: forall acc x,
  f x (fold_right f [] acc) = fold_right f [] (f acc x)

  f x (fold_right f [] acc)
=   { evaluation }
  f x acc

  fold_right f [] (f acc x)
=   { evaluation }
  f acc x
=   { commutativity of f }
  f x acc

Inductive case: lst = h :: t
IH: forall acc x,
  f x (fold_right f t acc) = fold_right f t (f acc x)
Show: forall acc x,
  f x (fold_right f (h :: t) acc) = fold_right f (h :: t) (f acc x)

  f x (fold_right f (h :: t) acc)
=  { evaluation }
  f x (f h (fold_right f t acc))
=  { interchange lemma }
  f h (f x (fold_right f t acc))
=  { IH }
  f h (fold_right f t (f acc x))

  fold_right f (h :: t) (f acc x)
=   { evaluation }
  f h (fold_right f t (f acc x))

QED

Now that the lemma is completed, we can resume the proof of the theorem. We’ll restart at the beginning of the inductive case:
现在引理已经完成,我们可以继续证明定理。我们将从归纳案例的开头重新开始:

Inductive case: lst = h :: t
IH: forall acc,
  fold_left f acc t = fold_right f t acc
Show: forall acc,
  fold_left f acc (h :: t) = fold_right f (h :: t) acc

  fold_left f acc (h :: t)
=   { evaluation }
  fold_left f (f acc h) t
=   { IH with acc := f acc h }
  fold_right f t (f acc h)

  fold_right f (h :: t) acc
=   { evaluation }
  f h (fold_right f t acc)
=   { lemma with x := h and lst := t }
  fold_right f t (f acc h)

QED

It took two inductions to prove the theorem, but we succeeded! Now we know that the behavior we observed with + wasn’t a fluke: any commutative and associative operator causes fold_left and fold_right to get the same answer.
证明这个定理需要两次归纳,但我们成功了!现在我们知道,我们在 + 中观察到的行为并不是侥幸:任何交换和结合运算符都会导致 fold_leftfold_right 得到相同的答案。

6.8.4. Induction on Trees
6.8.4. 树归纳法 ¶

Lists and binary trees are similar when viewed as data types. Here are the definitions of both, aligned for comparison:
当作为数据类型来看时,列表和二叉树是相似的。以下是两者的定义,为了比较而对齐:

type 'a list = []   | ( :: ) of           'a * 'a list
type 'a tree = Leaf | Node   of 'a tree * 'a * 'a tree
type 'a list = [] | (::) of 'a * 'a list
type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree

Both have a constructor that represents “empty”, and both have a constructor that combines a value of type 'a together with another instance of the data type. The only real difference is that ( :: ) takes just one list, whereas Node takes two trees.
两者都有一个表示“空”的构造函数,并且都有一个将 'a 类型的值与该数据类型的另一个实例组合在一起的构造函数。唯一真正的区别是 ( :: ) 仅采用一个列表,而 Node 采用两棵树。

The induction principle for binary trees is therefore very similar to the induction principle for lists, except that with binary trees we get two inductive hypotheses, one for each subtree:
因此,二叉树的归纳原理与列表的归纳原理非常相似,除了二叉树我们得到两个归纳假设,每个子树一个:

forall properties P,
  if P(Leaf),
  and if forall l v r, (P(l) and P(r)) implies P(Node (l, v, r)),
  then forall t, P(t)

An inductive proof for binary trees therefore has the following structure:
因此,二叉树的归纳证明具有以下结构:

Proof: by induction on t.
P(t) = ...

Base case: t = Leaf
Show: P(Leaf)

Inductive case: t = Node (l, v, r)
IH1: P(l)
IH2: P(r)
Show: P(Node (l, v, r))

Let’s try an example of this kind of proof. Here is a function that creates the mirror image of a tree, swapping its left and right subtrees at all levels:
让我们尝试举一个此类证明的例子。这是一个创建树镜像的函数,在所有级别交换其左子树和右子树:

let rec reflect = function
  | Leaf -> Leaf
  | Node (l, v, r) -> Node (reflect r, v, reflect l)
val reflect : 'a tree -> 'a tree = <fun>

For example, these two trees are reflections of each other:
例如,这两棵树是彼此的反射:

     1               1
   /   \           /   \
  2     3         3     2
 / \   / \       / \   / \
4   5 6   7     7   6 5   4

If you take the mirror image of a mirror image, you should get the original back. That means reflection is an involution, which is any function f such that f (f x) = x. Another example of an involution is multiplication by negative one on the integers.
如果您拍摄了镜像的镜像,您应该取回原件。这意味着反射是一个对合,它是任何满足 f (f x) = x 的函数 f 。求和的另一个例子是整数乘以负数。

Let’s prove that reflect is an involution.
让我们证明 reflect 是一个对合。

Claim: forall t, reflect (reflect t) = t

Proof: by induction on t.
P(t) = reflect (reflect t) = t

Base case: t = Leaf
Show: reflect (reflect Leaf) = Leaf

  reflect (reflect Leaf)
=   { evaluation }
  reflect Leaf
=   { evaluation }
  Leaf

Inductive case: t = Node (l, v, r)
IH1: reflect (reflect l) = l
IH2: reflect (reflect r) = r
Show: reflect (reflect (Node (l, v, r))) = Node (l, v, r)

  reflect (reflect (Node (l, v, r)))
=   { evaluation }
  reflect (Node (reflect r, v, reflect l))
=   { evaluation }
  Node (reflect (reflect l), v, reflect (reflect r))
=   { IH1 }
  Node (l, v, reflect (reflect r))
=   { IH2 }
  Node (l, v, r)

QED

Induction on trees is really no more difficult than induction on lists or natural numbers. Just keep track of the inductive hypotheses, using our stylized proof notation, and it isn’t hard at all.
对树的归纳实际上并不比对列表或自然数的归纳更困难。只需使用我们的程式化证明符号来跟踪归纳假设,这并不难。

6.8.5. Induction Principles for All Variants
6.8.5. 所有变体的归纳原则 ¶

We’ve now seen induction principles for nat, list, and tree. Generalizing from what we’ve seen, each constructor of a variant either generates a base case for the inductive proof, or an inductive case. And, if a constructor itself carries values of that data type, each of those values generates an inductive hypothesis. For example:
我们现在已经了解了 natlisttree 的归纳原理。从我们所看到的概括来看,变体的每个构造函数要么生成归纳证明的基本情况,要么生成归纳情况。而且,如果构造函数本身携带该数据类型的值,则每个值都会生成一个归纳假设。例如:

  • Z, [], and Leaf all generated base cases.
    Z[]Leaf 所有生成的基本情况。

  • S, ::, and Node all generated inductive cases.
    S::Node 所有生成的归纳案例。

  • S and :: each generated one IH, because each carries one value of the data type.
    S:: 各生成一个IH,因为它们各自携带该数据类型的一个值。

  • Node generated two IHs, because it carries two values of the data type.
    Node 生成了两个 IH,因为它携带了两个数据类型的值。

As an example of an induction principle for a more complicated type, let’s consider a type that represents the syntax of a mathematical expression. You might recall from an earlier data structures course that trees can be used for that purpose.
作为更复杂类型的归纳原理的示例,让我们考虑一个表示数学表达式语法的类型。您可能还记得之前的数据结构课程中提到过树可以用于此目的。

Suppose we have the following expr type, which is a kind of tree, to represent expressions with integers, Booleans, unary operators, and binary operators:
假设我们有以下 expr 类型,它是一种树,用于表示具有整数、布尔值、一元运算符和二元运算符的表达式:

type uop =
  | UMinus

type bop =
  | BPlus
  | BMinus
  | BLeq

type expr =
  | Int of int
  | Bool of bool
  | Unop of uop * expr
  | Binop of expr * bop * expr
type uop = UMinus
type bop = BPlus | BMinus | BLeq
type expr =
    Int of int
  | Bool of bool
  | Unop of uop * expr
  | Binop of expr * bop * expr

For example, the expression 5 < 6 would be represented as Binop (Int 5, BLeq, Int 6). We’ll see more examples of this kind of representation later in the book when we study interpreters.
例如,表达式 5 < 6 将表示为 Binop (Int 5, BLeq, Int 6) 。当我们在本书后面研究口译员时,我们会看到更多这种表示的例子。

The induction principle for expr is:
expr 的归纳原理是:

forall properties P,
  if forall i, P(Int i)
  and forall b, P(Bool b)
  and forall u e, P(e) implies P(Unop (u, e))
  and forall b e1 e2, (P(e1) and P(e2)) implies P(Binop (e1, b, e2))
  then forall e, P(e)

There are two base cases, corresponding to the two constructors that don’t carry an expr. There are two inductive cases, corresponding to the two constructors that do carry exprs. Unop gets one IH, whereas Binop gets two IHs, because of the number of exprs that each carries.
有两种基本情况,对应于两个不带有 expr 的构造函数。有两种归纳情况,对应于两个带有 expr 的构造函数。 Unop 获得一个 IH,而 Binop 获得两个 IH,因为每个 IH 携带的 expr 数量不同。

6.8.6. Induction and Recursion
6.8.6. 归纳和递归 ¶

Inductive proofs and recursive programs bear a striking similarity. In a sense, an inductive proof is a recursive program that shows how to construct evidence for a theorem involving an algebraic data type (ADT). The structure of an ADT determines the structure of proofs and programs:
归纳证明和递归程序有着惊人的相似之处。从某种意义上说,归纳证明是一个递归程序,它展示了如何为涉及代数数据类型(ADT)的定理构建证据。 ADT 的结构决定了证明和程序的结构:

  • The constructors of an ADT are the organizational principle of both proofs and programs. In a proof, we have a base or inductive case for each constructor. In a program, we have a pattern-matching case for each constructor.
    ADT 的构造者是证明和程序的组织原则。在证明中,我们为每个构造函数都有一个基本或归纳案例。在程序中,我们为每个构造函数都有一个模式匹配案例。

  • The use of recursive types in an ADT determine where recursion occurs in both proofs and programs. By “recursive type”, we mean the occurrence of the type in its own definition, such as the second 'a list in type 'a list = [] | ( :: ) 'a * 'a list. Such occurrences lead to “smaller” values of a type occurring inside larger values. In a proof, we apply the inductive hypothesis upon reaching such a smaller value. In a program, we recurse on the smaller value.
    ADT 中递归类型的使用决定了证明和程序中递归发生的位置。 “递归类型”是指该类型在其自己的定义中出现,例如 type 'a list = [] | ( :: ) 'a * 'a list 中的第二个 'a list 。这种情况的出现会导致某个类型的“较小”值出现在较大值内。在证明中,我们在达到如此小的值时应用归纳假设。在程序中,我们对较小的值进行递归。

6.9. Algebraic Specification
6.9. 代数规范 ¶

Next let’s tackle a bigger challenge: proving the correctness of a data structure, such as a stack, queue, or set.
接下来让我们应对一个更大的挑战:证明数据结构(例如堆栈、队列或集合)的正确性。

Correctness proofs always need specifications. In proving the correctness of iterative factorial, we used recursive factorial as a specification. By analogy, we could provide two implementations of a data structure—one simple, the other complex and efficient—and prove that the two are equivalent. That would require us to introduce ways to translate between the two implementations. For example, we could prove the correctness of a map implemented with an efficient balanced binary search tree relative to an implementation as an inefficient association list, by defining functions to convert trees to lists. Such an approach is certainly valid, but it doesn’t lead to new ideas about verification for us to study.
正确性证明总是需要规范。在证明迭代阶乘的正确性时,我们使用递归阶乘作为规范。通过类比,我们可以提供数据结构的两种实现——一种简单,另一种复杂且高效——并证明两者是等价的。这需要我们引入在两种实现之间进行转换的方法。例如,通过定义将树转换为列表的函数,我们可以证明使用高效平衡二叉搜索树实现的映射相对于低效关联列表实现的正确性。这样的做法当然是有效的,但是它并没有带来新的验证思路可供我们研究。

Instead, we will pursue a different approach based on equational specifications, aka algebraic specifications. The idea with these is to
相反,我们将寻求一种基于方程规范(又称为代数规范)的不同方法。这些的想法是

  • define the types of the data structure operations, and
    定义数据结构操作的类型,以及

  • to write a set of equations that define how the operations interact with one another.
    编写一组定义操作如何相互作用的方程。

The reason the word “algebra” shows up here is (in part) that this type-and-equation based approach is something we learned in high-school algebra. For example, here is a specification for some operators:
“代数”这个词出现在这里的原因(部分)是这种基于类型和方程的方法是我们在高中代数中学到的东西。例如,以下是一些运算符的规范:

0 : int
1 : int
- : int -> int
+ : int -> int -> int
* : int -> int -> int

(a + b) + c = a + (b + c)
a + b = b + a
a + 0 = a
a + (-a) = 0
(a * b) * c = a * (b * c)
a * b = b * a
a * 1 = a
a * 0 = 0
a * (b + c) = a * b + a * c

The types of those operators, and the associated equations, are facts learned when studying algebra.
这些运算符的类型以及相关的方程是学习代数时学到的事实。

Our goal is now to write similar specifications for data structures, and use them to reason about the correctness of implementations.
我们现在的目标是为数据结构编写类似的规范,并使用它们来推理实现的正确性。

6.9.1. Example: Stacks 6.9.1. 示例:堆栈 ¶

Here are a few familiar operations on stacks along with their types.
以下是一些熟悉的堆栈操作及其类型。

module type Stack = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val peek : 'a t -> 'a
  val push : 'a -> 'a t -> 'a t
  val pop : 'a t -> 'a t
end
module type Stack =
  sig
    type 'a t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val peek : 'a t -> 'a
    val push : 'a -> 'a t -> 'a t
    val pop : 'a t -> 'a t
  end

As usual, there is a design choice to be made with peek etc. about what to do with empty stacks. Here we have not used option, which suggests that peek will raise an exception on the empty stack. So we are cautiously relaxing our prohibition on exceptions.
像往常一样,需要对 peek 等进行设计选择,了解如何处理空堆栈。这里我们没有使用 option ,这表明 peek 将在空堆栈上引发异常。因此,我们正在谨慎地放宽对例外的禁止。

In the past we’ve given these operations specifications in English, e.g.,
过去我们用英语给出了这些操作规范,例如,

  (** [push x s] is the stack [s] with [x] pushed on the top. *)
  val push : 'a -> 'a stack -> 'a stack

But now, we’ll write some equations to describe how the operations work:
但现在,我们将编写一些方程来描述操作的工作原理:

1. is_empty empty = true
2. is_empty (push x s) = false
3. peek (push x s) = x
4. pop (push x s) = s

(Later we’ll return to the question of how to design such equations.) The variables appearing in these equations are implicitly universally quantified. Here’s how to read each equation:
(稍后我们将回到如何设计这样的方程的问题。)这些方程中出现的变量是隐式普遍量化的。以下是如何阅读每个方程:

  1. is_empty empty = true. The empty stack is empty.
    is_empty empty = true 。空栈是空的。

  2. is_empty (push x s) = false. A stack that has just been pushed is non-empty.
    is_empty (push x s) = false 。刚刚入栈的栈非空。

  3. peek (push x s) = x. Pushing then immediately peeking yields whatever value was pushed.
    peek (push x s) = x 。推送然后立即查看会产生推送的任何值。

  4. pop (push x s) = s. Pushing then immediately popping yields the original stack.
    pop (push x s) = s 。压入然后立即弹出会产生原始堆栈。

Just with these equations alone, we already can deduce a lot about how any sequence of stack operations must work. For example,
仅凭这些方程,我们就已经可以推断出许多有关堆栈操作序列必须如何工作的信息。例如,

  peek (pop (push 1 (push 2 empty)))
=   { equation 4 }
  peek (push 2 empty)
=   { equation 3 }
  2

And peek empty doesn’t equal any value according to the equations, since there is no equation of the form peek empty = .... All that is true regardless of the stack implementation that is chosen: any correct implementation must cause the equations to hold.
根据方程, peek empty 不等于任何值,因为不存在 peek empty = ... 形式的方程。无论选择哪种堆栈实现,所有这些都是正确的:任何正确的实现都必须使方程成立。

Suppose we implemented stacks as lists, as follows:
假设我们将堆栈实现为列表,如下所示:

module ListStack = struct
  type 'a t = 'a list
  let empty = []
  let is_empty = function [] -> true | _ -> false
  let peek = List.hd
  let push = List.cons
  let pop = List.tl
end
module ListStack :
  sig
    type 'a t = 'a list
    val empty : 'a list
    val is_empty : 'a list -> bool
    val peek : 'a list -> 'a
    val push : 'a -> 'a list -> 'a list
    val pop : 'a list -> 'a list
  end

Next we could prove that each equation holds of the implementation. All these proofs are quite easy by now, and proceed entirely by evaluation. For example, here’s a proof of equation 3:
接下来我们可以证明每个方程都适用于实现。到目前为止,所有这些证明都很容易,并且完全通过评估进行。例如,下面是方程 3 的证明:

  peek (push x s)
=   { evaluation }
  peek (x :: s)
=   { evaluation }
  x

6.9.2. Example: Queues 6.9.2. 示例:队列 ¶

Stacks were easy. How about queues? Here is the specification:
堆栈很容易。排队怎么样?这是规格:

module type Queue = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val front : 'a t -> 'a
  val enq : 'a -> 'a t -> 'a t
  val deq : 'a t -> 'a t
end
module type Queue =
  sig
    type 'a t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val front : 'a t -> 'a
    val enq : 'a -> 'a t -> 'a t
    val deq : 'a t -> 'a t
  end
1.  is_empty empty = true
2.  is_empty (enq x q) = false
3a. front (enq x q) = x            if is_empty q = true
3b. front (enq x q) = front q      if is_empty q = false
4a. deq (enq x q) = empty          if is_empty q = true
4b. deq (enq x q) = enq x (deq q)  if is_empty q = false

The types of the queue operations are actually identical to the types of the stack operations. Here they are, side-by-side for comparison:
队列操作的类型实际上与堆栈操作的类型相同。在这里,将它们并排进行比较:

module type Stack = sig            module type Queue = sig
  type 'a t                          type 'a t
  val empty : 'a t                   val empty : 'a t
  val is_empty : 'a t -> bool        val is_empty : 'a t -> bool
  val peek : 'a t -> 'a              val front : 'a t -> 'a
  val push : 'a -> 'a t -> 'a t      val enq : 'a -> 'a t -> 'a t
  val pop : 'a t -> 'a t             val deq : 'a t -> 'a t
end                                end

Look at each line: though the operation may have a different name, its type is the same. Obviously, the types alone don’t tell us enough about the operations. But the equations do! Here’s how to read each equation:
查看每一行:虽然操作可能有不同的名称,但其类型是相同的。显然,仅类型并不能告诉我们足够的操作信息。但方程式确实如此!以下是如何阅读每个方程:

  1. The empty queue is empty.
    空队列是空的。

  2. Enqueueing makes a queue non-empty.
    入队使队列非空。

  3. Enqueueing x on an empty queue makes x the front element. But if the queue isn’t empty, enqueueing doesn’t change the front element.
    x 放入空队列会使 x 成为最前面的元素。但如果队列不为空,则排队不会更改前面的元素。

  4. Enqueueing then dequeueing on an empty queue leaves the queue empty. But if the queue isn’t empty, the enqueue and dequeue operations can be swapped.
    在空队列上入队然后出队会使队列为空。但如果队列不为空,则可以交换入队和出队操作。

For example, 例如,

  front (deq (enq 1 (enq 2 empty)))
=   { equation 4b }
  front (enq 1 (deq (enq 2 empty)))
=   { equation 4a }
  front (enq 1 empty)
=   { equation 3a }
  1

And front empty doesn’t equal any value according to the equations.
根据等式, front empty 不等于任何值。

Implementing a queue as a list results in an implementation that is easy to verify just with evaluation.
将队列实现为列表会导致仅通过评估即可轻松验证的实现。

module ListQueue : Queue = struct
  type 'a t = 'a list
  let empty = []
  let is_empty q = q = []
  let front = List.hd
  let enq x q = q @ [x]
  let deq = List.tl
end
module ListQueue : Queue

For example, 4a can be verified as follows:
例如4a可以这样验证:

  deq (enq x empty)
=   { evaluation of empty and enq}
  deq ([] @ [x])
=   { evaluation of @ }
  deq [x]
=   { evaluation of deq }
  []
=   { evaluation of empty }
  empty

And 4b, as follows:
以及4b,如下:

  deq (enq x q)
=  { evaluation of enq and deq }
  List.tl (q @ [x])
=  { lemma, below, and q <> [] }
  (List.tl q) @ [x]

  enq x (deq q)
=  { evaluation }
  (List.tl q) @ [x]

Here is the lemma:
这是引理:

Lemma: if xs <> [], then List.tl (xs @ ys) = (List.tl xs) @ ys.
Proof: if xs <> [], then xs = h :: t for some h and t.

  List.tl ((h :: t) @ ys)
=   { evaluation of @ }
  List.tl (h :: (t @ ys))
=   { evaluation of tl }
  t @ ys

  (List.tl (h :: t)) @ ys
=   { evaluation of tl }
  t @ ys

QED

Note how the precondition in 3b and 4b of q not being empty ensures that we never have to deal with an exception being raised in the equational proofs.
请注意,3b 和 4b 中 q 不为空的前提条件如何确保我们永远不必处理等式证明中引发的异常。

6.9.3. Example: Batched Queues
6.9.3. 示例:批处理队列 ¶

Recall that batched queues represent a queue with two lists:
回想一下,批处理队列表示具有两个列表的队列:

module BatchedQueue = struct
  (* AF: [(o, i)] represents the queue [o @ (List.rev i)].
     RI: if [o] is empty then [i] is empty. *)
  type 'a t = 'a list * 'a list

  let empty = ([], [])

  let is_empty (o, i) = if o = [] then true else false

  let enq x (o, i) = if o = [] then ([x], []) else (o, x :: i)

  let front (o, _) = List.hd o

  let deq (o, i) =
    match List.tl o with
    | [] -> (List.rev i, [])
    | t -> (t, i)
end
module BatchedQueue :
  sig
    type 'a t = 'a list * 'a list
    val empty : 'a list * 'b list
    val is_empty : 'a list * 'b -> bool
    val enq : 'a -> 'a list * 'a list -> 'a list * 'a list
    val front : 'a list * 'b -> 'a
    val deq : 'a list * 'a list -> 'a list * 'a list
  end

This implementation is superficially different from the earlier implementation we gave, in that it uses pairs instead of records, and it raises the built-in exception Failure instead of a custom exception Empty.
此实现与我们之前提供的实现表面上有所不同,因为它使用对而不是记录,并且引发内置异常 Failure 而不是自定义异常 Empty

Is this implementation correct? We need only verify the equations to find out.
这个实现正确吗?我们只需验证方程即可找到答案。

First, a lemma: 首先,一个引理:

Lemma:  if is_empty q = true, then q = empty.
Proof:  Since is_empty q = true, it must be that q = (f, b) and f = [].
By the RI, it must also be that b = [].  Thus q = ([], []) = empty.
QED

Verifying equation 1: 验证方程 1:

  is_empty empty
=   { eval empty }
  is_empty ([], [])
=   { eval is_empty }
  [] = []
=   { eval = }
  true

Verifying equation 2: 验证方程 2:

  is_empty (enq x q) = false
=   { eval enq }
  is_empty (if f = [] then [x], [] else f, x :: b)

case analysis: f = []

  is_empty (if f = [] then [x], [] else f, x :: b)
=   { eval if, f = [] }
  is_empty ([x], [])
=   { eval is_empty }
  [x] = []
=   { eval = }
  false

case analysis: f = h :: t

  is_empty (if f = [] then [x], [] else f, x :: b)
=   { eval if, f = h :: t }
  is_empty (h :: t, x :: b)
=   { eval is_empty }
  h :: t = []
=   { eval = }
  false

Verifying equation 3a: 验证方程 3a:

  front (enq x q) = x
=   { emptiness lemma }
  front (enq x ([], []))
=   { eval enq }
  front ([x], [])
=   { eval front }
  x

Verifying equation 3b: 验证方程 3b:

  front (enq x q)
=   { rewrite q as (h :: t, b), because q is not empty }
  front (enq x (h :: t, b))
=   { eval enq }
  front (h :: t, x :: b)
=   { eval front }
  h

  front q
=   { rewrite q as (h :: t, b), because q is not empty }
  front (h :: t, b)
=   { eval front }
  h

Verifying equation 4a: 验证方程 4a:

  deq (enq x q)
=   { emptiness lemma }
  deq (enq x ([], []))
=   { eval enq }
  deq ([x], [])
=   { eval deq }
  List.rev [], []
=   { eval rev }
  [], []
=   { eval empty }
  empty

Verifying equation 4b: 验证方程 4b:

Show: deq (enq x q) = enq x (deq q)  assuming is_empty q = false.
Proof: Since is_empty q = false, q must be (h :: t, b).

Case analysis:  t = [], b = []

  deq (enq x q)
=   { rewriting q as ([h], []) }
  deq (enq x ([h], []))
=   { eval enq }
  deq ([h], [x])
=   { eval deq }
  List.rev [x], []
=   { eval rev }
  [x], []

  enq x (deq q)
=   { rewriting q as ([h], []) }
  enq x (deq ([h], []))
=   { eval deq }
  enq x (List.rev [], [])
=   { eval rev }
  enq x ([], [])
=   { eval enq }
  [x], []

Case analysis:  t = [], b = h' :: t'

  deq (enq x q)
=   { rewriting q as ([h], h' :: t') }
  deq (enq x ([h], h' :: t'))
=   { eval enq }
  deq ([h], x :: h' :: t')
=   { eval deq }
  (List.rev (x :: h' :: t'), [])

  enq x (deq q)
=   { rewriting q as ([h], h' :: t') }
  enq x (deq ([h], h' :: t'))
=   { eval deq }
  enq x (List.rev (h' :: t'), [])
=   { eval enq }
  (List.rev (h' :: t'), [x])

STUCK

Wait, we just got stuck! (List.rev (x :: h' :: t'), []) and (List.rev (h' :: t'), [x]) are different. But, abstractly, they do represent the same queue: (List.rev t') @ [h'; x].
等等,我们刚刚被困住了! (List.rev (x :: h' :: t'), [])(List.rev (h' :: t'), [x]) 不同。但是,抽象地讲,它们确实代表相同的队列: (List.rev t') @ [h'; x]

To solve this problem, we will adopt the following equation for representation types:
为了解决这个问题,我们将采用以下表示类型等式:

e = e'   if  AF(e) = AF(e')

That equation allows us to conclude that the two differing expressions are equal:
该方程让我们得出结论:两个不同的表达式是相等的:

  AF((List.rev (h' :: t'), [x]))
=   { apply AF } 
  List.rev (h' :: t') @ List.rev [x]
=   { rev distributes over @, an exercise in the previous lecture }
  List.rev ([x] @ (h' :: t'))
=   { eval @ }
  List.rev (x :: h' :: t')
  
  AF((List.rev (x :: h' :: t'), []))
=   { apply AF }
  List.rev (x :: h' :: t') @ List.rev []
=   { eval rev }
  List.rev (x :: h' :: t') @ []
=   { eval @ }
  List.rev (x :: h' :: t')

Now we are unstuck:
现在我们摆脱困境了:

  (List.rev (h' :: t'), [x])
=   { AF equation }
  (List.rev (x :: h' :: t'), [])

There is one more case analysis remaining to finish the proof:
还剩下一个案例分析来完成证明:

Case analysis:  t = h' :: t'

  deq (enq x q)
=   { rewriting q as (h :: h' :: t', b) }
  deq (enq x (h :: h' :: t', b))
=   { eval enq }
  deq (h :: h' :: t, x :: b)
=   { eval deq }
  h' :: t, x :: b

  enq x (deq q)
=   { rewriting q as (h :: h' :: t', b) }
  enq x (deq (h :: h' :: t', b))
=   { eval deq }
  enq x (h' :: t', b)
=   { eval enq }
  h' :: t', x :: b

QED

That concludes our verification of the batched queue. Note that we had to add the extra equation involving the abstraction function to get the proofs to go through:
我们对批量队列的验证到此结束。请注意,我们必须添加涉及抽象函数的额外方程才能完成证明:

e = e'   if  AF(e) = AF(e')

and that we made use of the RI during the proof. The AF and RI really are important!
我们在证明过程中使用了 RI。 AF和RI真的很重要!

6.9.4. Designing Algebraic Specifications
6.9.4. 设计代数规范 ¶

For both stacks and queues we provided some equations as the specification. Designing those equations is, in part, a matter of thinking hard about the data structure. But there’s more to it than that.
对于堆栈和队列,我们​​提供了一些方程作为规范。设计这些方程在某种程度上需要认真思考数据结构。但还有更多的事情要做。

Every value of the data structure is constructed with some operations. For a stack, those operations are empty and push. There might be some pop operations involved, but those can be eliminated. For example, pop (push 1 (push 2 empty)) is really the same stack as push 2 empty. The latter is the canonical form of that stack: there are many other ways to construct it, but that is the simplest. Indeed, every possible stack value can be constructed just with empty and push. Similarly, every possible queue value can be constructed just with empty and enq: if there are deq operations involved, those can be eliminated.
数据结构的每个值都是通过一些操作构造的。对于堆栈,这些操作是 emptypush 。可能涉及一些 pop 操作,但这些操作可以消除。例如, pop (push 1 (push 2 empty)) 实际上与 push 2 empty 是同一个堆栈。后者是该堆栈的规范形式:还有许多其他方法来构造它,但这是最简单的。事实上,每个可能的堆栈值都可以仅使用 emptypush 来构造。类似地,每个可能的队列值都可以仅使用 emptyenq 构造:如果涉及 deq 操作,则可以消除这些操作。

Let’s categorize the operations of a data structure as follows:
我们将数据结构的操作分类如下:

  • Generators are those operations involved in creating a canonical form. They return a value of the data structure type. For example, empty, push, enq.
    生成器是涉及创建规范形式的那些操作。它们返回数据结构类型的值。例如, emptypushenq

  • Manipulators are operations that create a value of the data structure type, but are not needed to create canonical forms. For example, pop, deq.
    操纵杆是创建数据结构类型值的操作,但不需要创建规范形式。例如, popdeq

  • Queries do not return a value of the data structure type. For example, is_empty, peek, front.
    查询不返回数据结构类型的值。例如, is_emptypeekfront

Given such a categorization, we can design the equational specification of a data structure by applying non-generators to generators. For example: What does is_empty return on empty? on push? What does front return on enq? What does deq return on enq? etc.
给定这样的分类,我们可以通过将非生成器应用于生成器来设计数据结构的等式规范。例如: is_emptyempty 上返回什么?在 push 上? frontenq 上返回什么? deqenq 上返回什么? ETC。

So if there are n generators and m non-generators of a data structure, we would begin by trying to create n*m equations, one for each pair of a generator and non-generator. Each equation would show how to simplify an expression. In some cases we might need a couple equations, depending on the result of some comparison. For example, in the queue specification, we have the following equations:
因此,如果数据结构中有 n 生成器和 m 非生成器,我们将首先尝试创建 n*m 方程,每对一个发电机和非发电机。每个方程都会展示如何简化表达式。在某些情况下,我们可能需要几个方程,具体取决于某些比较的结果。例如,在队列规范中,我们有以下等式:

  1. is_empty empty = true: this is a non-generator is_empty applied to a generator empty. It reduces just to a Boolean value, which doesn’t involve the data structure type (queues) at all.
    is_empty empty = true :这是应用于生成器 empty 的非生成器 is_empty 。它只是简化为布尔值,根本不涉及数据结构类型(队列)。

  2. is_empty (enq x q) = false: a non-generator is_empty applied to a generator enq. Again it reduces simply to a Boolean value.
    is_empty (enq x q) = false :应用于生成器 enq 的非生成器 is_empty 。它再次简单地简化为布尔值。

  3. There are two subcases.
    有两个子情况。

    • front (enq x q) = x, if is_empty q = true. A non-generator front applied to a generator enq. It reduces to x, which is a smaller expression than the original front (enq x q).
      front (enq x q) = x ,如果 is_empty q = true 。非生成器 front 应用于生成器 enq 。它简化为 x ,这是一个比原始 front (enq x q) 更小的表达式。

    • front (enq x q) = front q, if is_empty q = false. This similarly reduces to a smaller expression.
      front (enq x q) = front qif is_empty q = false 。这同样简化为更小的表达式。

  4. Again, there are two subcases.
    同样,有两个子情况。

    • deq (enq x q) = empty, if is_empty q = true. This simplifies the original expression by reducing it to empty.
      deq (enq x q) = empty ,如果 is_empty q = true 。这将原始表达式简化为 empty

    • deq (enq x q) = enq x (deq q), if is_empty q = false. This simplifies the original expression by reducing it to an generator applied to a smaller argument, deq q instead of deq (enq x q).
      deq (enq x q) = enq x (deq q) ,如果 is_empty q = false 。这通过将原始表达式简化为应用于较小参数 deq q 而不是 deq (enq x q) 的生成器来简化原始表达式。

We don’t usually design equations involving pairs of non-generators. Sometimes pairs of generators are needed, though, as we will see in the next example.
我们通常不会设计涉及非生成元对的方程。不过,有时需要成对的生成器,正如我们将在下一个示例中看到的那样。

Example: Sets. Here is a small interface for sets:
示例:集合。这是集合的一个小界面:

module type Set = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val add : 'a -> 'a t -> 'a t
  val mem : 'a -> 'a t -> bool
  val remove : 'a -> 'a t -> 'a t
end
module type Set =
  sig
    type 'a t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val add : 'a -> 'a t -> 'a t
    val mem : 'a -> 'a t -> bool
    val remove : 'a -> 'a t -> 'a t
  end

The generators are empty and add. The only manipulator is remove. Finally, is_empty and mem are queries. So we should expect at least 2 * 3 = 6 equations, one for each pair of generator and non-generator. Here is an equational specification:
生成器是 emptyadd 。唯一的操纵器是 remove 。最后, is_emptymem 是查询。因此,我们应该期望至少有 2 * 3 = 6 个方程,每对生成器和非生成器一个。这是一个等式规范:

1.  is_empty empty = true
2.  is_empty (add x s) = false
3.  mem x empty = false
4a. mem y (add x s) = true                    if x = y
4b. mem y (add x s) = mem y s                 if x <> y
5.  remove x empty = empty
6a. remove y (add x s) = remove y s           if x = y
6b. remove y (add x s) = add x (remove y s)   if x <> y

Consider, though, these two sets:
不过,请考虑这两组:

  • add 0 (add 1 empty)

  • add 1 (add 0 empty)

They both intuitively represent the set {0,1}. Yet, we cannot prove that those two sets are equal using the above specification. We are missing an equation involving two generators:
它们都直观地表示集合 {0,1}。然而,我们无法使用上述规范证明这两个集合相等。我们缺少一个涉及两个生成器的方程:

7.  add x (add y s) = add y (add x s)

6.10. Summary 6.10. 小结 ¶

Documentation and testing are crucial to establishing the truth of what a correct program does. Documentation communicates to other humans the intent of the programmer. Testing communicates evidence about the success of the programmer.
文档和测试对于确定正确程序的作用至关重要。文档向其他人传达程序员的意图。测试传达了程序员成功的证据。

Good documentation provides several pieces: a summary, preconditions, postconditions (including errors), and examples. Documentation is written for two different audiences, clients and maintainers. The latter needs to know about abstraction functions and representation invariants.
好的文档提供了几个部分:摘要、前提条件、后置条件(包括错误)和示例。文档是为两种不同的受众编写的:客户和维护人员。后者需要了解抽象函数和表示不变量。

Testing methodologies include black-box, glass-box, and randomized tests. These are complementary, not orthogonal, approaches to developing correct code.
测试方法包括黑盒测试、玻璃盒测试和随机测试。这些是开发正确代码的补充方法,而不是正交方法。

Formal methods is an important link between mathematics and computer science. We can use techniques from discrete math, such as induction, to prove the correctness of functional programs. Equational reasoning makes the proofs relatively pleasant.
形式化方法是数学和计算机科学之间的重要纽带。我们可以使用离散数学的技术(例如归纳法)来证明函数式程序的正确性。等式推理使证明相对令人愉快。

Proving the correctness of imperative programs can be more challenging, because of the need to reason about mutable state. That can break equational reasoning. Instead, Hoare logic, named for Tony Hoare, is a common formal method for imperative programs. Dijkstra’s weakest precondition calculus is another.
由于需要推理可变状态,证明命令式程序的正确性可能更具挑战性。这可能会破坏等式推理。相反,以托尼·霍尔 (Tony Hoare) 命名的霍尔逻辑是命令式程序的常见形式方法。 Dijkstra 的最弱前提条件演算是另一个。

6.10.1. Terms and Concepts
6.10.1. 术语和概念 ¶

  • abstract value 抽象价值

  • abstraction by specification
    按规范抽象

  • abstraction function 抽象函数

  • algebraic specification 代数规范

  • asserting 断言

  • associative 联想性的

  • base case 基本情况

  • black box 黑盒子

  • boundary case 边界情况

  • bug

  • canonical form 规范形式

  • client 客户

  • code inspection 代码检查

  • code review 代码审查

  • code walkthrough 代码演练

  • comments 评论

  • commutative 可交换的

  • commutative diagram 交换图

  • concrete value 具体价值

  • conditional compilation 条件编译

  • consumer 消费者

  • correctness 正确性

  • data abstraction 数据抽象

  • debugging by scientific method
    科学方法调试

  • defensive programming 防御性编程

  • equation 方程

  • equational reasoning 等式推理

  • example clause 示例子句

  • extensionality 外延性

  • failure 失败

  • fault 过错

  • formal methods 形式化方法

  • formal methods 形式化方法

  • generator 发电机

  • glass box 玻璃盒

  • identity 身份

  • implementer 实施者

  • induction 就职

  • induction hypothesis 归纳假说

  • induction principle 感应原理

  • inductive case 感应案例

  • inputs for classes of output
    输出类别的输入

  • inputs that satisfy precondition
    满足前提条件的输入

  • inputs that trigger exceptions
    触发异常的输入

  • iterative 迭代的

  • locality 地点

  • manipulator 机械手

  • many to one 多对一

  • minimal test case 最小测试用例

  • modifiability 可修改性

  • natural numbers 自然数

  • pair programming 结对编程

  • partial 部分的

  • partial correctness 部分正确

  • partial function 部分功能

  • path coverage 路径覆盖

  • paths through implementation
    实施路径

  • paths through specification
    通过规范的路径

  • postcondition 后置条件

  • postcondition 后置条件

  • precondition 前提

  • precondition 前提

  • producer 制片人

  • query 询问

  • raises clause 提出条款

  • randomized testing 随机测试

  • regression testing 回归测试

  • rely 依靠

  • rep ok 代表好的

  • representation invariant 表示不变性

  • representation type 表示类型

  • representative inputs 代表性投入

  • requires clause 需要子句

  • returns clause 退货条款

  • satisfaction 满意

  • social methods 社会方法

  • specification 规格

  • specification 规格

  • testing 测试

  • total correctness 完全正确

  • total function 总功能

  • typical input 典型输入

  • validation 验证

  • verification

  • well-founded 有根据的

6.10.2. Further Reading
6.10.2. 延伸阅读 ¶

  • Program Development in Java: Abstraction, Specification, and Object-Oriented Design, chapters 3, 5, and 9, by Barbara Liskov with John Guttag.
    《Java 程序开发:抽象、规范和面向对象设计》,第 3、5 和 9 章,作者:Barbara Liskov 和 John Guttag。

  • The Functional Approach to Programming, section 3.4. Guy Cousineau and Michel Mauny. Cambridge, 1998.
    函数式编程方法,第 3.4 节。盖伊·库西诺和米歇尔·莫尼。剑桥,1998。

  • ML for the Working Programmer, second edition, chapter 6. L.C. Paulson. Cambridge, 1996.
    面向工作程序员的 ML,第二版,第 6 章。保尔森。剑桥,1996。

  • Thinking Functionally with Haskell, chapter 6. Richard Bird. Cambridge, 2015.
    用 Haskell 进行函数式思考,第 6 章。理查德·伯德。剑桥,2015。

  • Software Foundations, volume 1, chapters Basic, Induction, Lists, Poly. Benjamin Pierce et al. https://softwarefoundations.cis.upenn.edu/
    软件基础,​​第 1 卷,基础、归纳、列表、聚合章节。本杰明·皮尔斯等人。 https://softwarefoundations.cis.upenn.edu/

  • “Algebraic Specifications”, Robert McCloskey, https://www.cs.scranton.edu/~mccloske/courses/se507/alg_specs_lec.html.
    “代数规范”,Robert McCloskey,https://www.cs.scranton.edu/~mccloske/courses/se507/alg_specs_lec.html。

  • Software Engineering: Theory and Practice, third edition, section 4.5. Shari Lawrence Pfleeger and Joanne M. Atlee. Prentice Hall, 2006.
    软件工程:理论与实践,第三版,第 4.5 节。莎莉·劳伦斯·普弗利格和乔安妮·M·阿特利。普伦蒂斯·霍尔,2006。

  • “Algebraic Semantics”, chapter 12 of Formal Syntax and Semantics of Programming Languages, Kenneth Slonneger and Barry L. Kurtz, Addison-Wesley, 1995.
    “代数语义”,《编程语言的形式语法和语义》第 12 章,Kenneth Slonneger 和 Barry L. Kurtz,Addison-Wesley,1995 年。

  • “Algebraic Semantics”, Muffy Thomas. Chapter 6 in Programming Language Syntax and Semantics, David Watt, Prentice Hall, 1991.
    “代数语义”,莫菲·托马斯。 《编程语言语法和语义》第 6 章,David Watt,Prentice Hall,1991 年。

  • Fundamentals of Algebraic Specification 1: Equations and Initial Semantics. H. Ehrig and B. Mahr. Springer-Verlag, 1985.
    代数规范 1 的基础:方程和初始语义。 H. Ehrig 和 B. Mahr。施普林格出版社,1985。

6.10.3. Acknowledgments
6.10.3. 致谢 ¶

Our treatment of formal methods is inspired by and indebted to course materials for Princeton COS 326 by David Walker et al.
我们对形式化方法的处理受到 David Walker 等人的 Princeton COS 326 课程材料的启发和借鉴。

Our example algebraic specifications are based on McCloskey’s. The terminology of “generator”, “manipulator”, and “query” is based on Pfleeger and Atlee.
我们的示例代数规范基于 McCloskey 的。 “生成器”、“操纵器”和“查询”的术语基于 Pfleeger 和 Atlee。

Many of our exercises on formal methods are inspired by Software Foundations, volume 1.
我们关于形式化方法的许多练习都受到《软件基础》第一卷的启发。

6.11. Exercises 6.11. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: spec game [★★★]
练习:规格游戏[★★★]

Pair up with another programmer and play the specification game with them. Take turns being the specifier and the devious programmer. Here are some suggested functions you could use:
与另一位程序员配对并与他们一起玩规范游戏。轮流担任指定者和狡猾的程序员。以下是一些您可以使用的建议功能:

  • num_vowels : string -> int

  • is_sorted : 'a list -> bool

  • sort : 'a list -> 'a list

  • max : 'a list -> 'a

  • is_prime : int -> bool

  • is_palindrome : string -> bool

  • second_largest : int list -> int

  • depth : 'a tree -> int


Exercise: poly spec [★★★]
练习:多项式规格[★★★]

Let’s create a data abstraction for single-variable integer polynomials of the form
让我们为以下形式的单变量整数多项式创建数据抽象

cnxn++c1x+c0.

Let’s assume that the polynomials are dense, meaning that they contain very few coefficients that are zero. Here is an incomplete interface for polynomials:
我们假设多项式是稠密的,这意味着它们包含很少的零系数。这是一个不完整的多项式接口:

(** [Poly] represents immutable polynomials with integer coefficients. *)
module type Poly = sig
  (** [t] is the type of polynomials *)
  type t

  (** [eval x p] is [p] evaluated at [x]. Example: if [p] represents
      $3x^3 + x^2 + x$, then [eval 10 p] is [3110]. *)
  val eval : int -> t -> int
end

Finish the design of Poly by adding more operations to the interface. Consider what operations would be useful to a client of the abstraction:
通过向界面添加更多操作来完成 Poly 的设计。考虑哪些操作对抽象的客户端有用:

  • How would they create polynomials?
    他们将如何创建多项式?

  • How would they combine polynomials to get new polynomials?
    他们如何组合多项式来得到新的多项式?

  • How would they query a polynomial to find out what it represents?
    他们如何查询多项式来找出它代表什么?

Write specification comments for the operations that you invent. Keep in mind the spec game as you write them: could a devious programmer subvert your intentions?
为您发明的操作编写规范注释。当你编写规范时,请记住规范游戏:狡猾的程序员会颠覆你的意图吗?


Exercise: poly impl [★★★]
练习:多项式规格实现[★★★]

Implement your specification of Poly. As part of your implementation, you will need to choose a representation type t. Hint: recalling that our polynomials are dense might guide you in choosing a representation type that makes for an easier implementation.
实现您的 Poly 规范。作为实现的一部分,您需要选择表示类型 t 。提示:回想一下我们的多项式是稠密的,可能会指导您选择一种更容易实现的表示类型。


Exercise: interval arithmetic [★★★★]
练习:区间算术[★★★★]

Specify and implement a data abstraction for interval arithmetic. Be sure to include the abstraction function, representation invariant, and rep_ok. Also implement a to_string function and a format that can be installed in the top level with #install_printer.
指定并实现区间算术的数据抽象。请务必包含抽象函数、表示不变式和 rep_ok 。还实现一个 to_string 函数和一个 format ,可以使用 #install_printer 安装在顶层。


Exercise: function maps [★★★★]
练习:函数映射[★★★★]

Implement a map (aka dictionary) data structure with abstract type ('k, 'v) t. As the representation type, use 'k -> 'v. That is, a map is represented as an OCaml function from keys to values. Document the AF. You do not need an RI. Your solution will make heavy use of higher-order functions. Provide at least these values and operations: empty, mem, find, add, remove.
实现具有抽象类型 ('k, 'v) t 的映射(又名字典)数据结构。使用 'k -> 'v 作为表示类型。也就是说,映射表示为从键到值的 OCaml 函数。记录 AF。您不需要 RI。您的解决方案将大量使用高阶函数。至少提供以下值和操作: emptymemfindaddremove


Exercise: set black box [★★★]
练习:集合黑盒测试[★★★]

Go back to the implementation of sets with lists in the previous chapter. Based on the specification comments of Set, write an OUnit test suite for ListSet that does black-box testing of all its operations.
回到上一章中带有列表的集合的实现。根据 Set 的规范注释,为 ListSet 编写一个 OUnit 测试套件,对其所有操作进行黑盒测试。


Exercise: set glass box [★★★]
练习:集合白盒测试[★★★]

Achieve as close to 100% code coverage with Bisect as you can for ListSet and UniqListSet.
使用 Bisect 实现与 ListSetUniqListSet 一样接近 100% 的代码覆盖率。


Exercise: random lists [★★★]
练习:随机值列表[★★★]

Use QCheck.Gen.generate1 to generate a list whose length is between 5 and 10, and whose elements are integers between 0 and 100. Then use QCheck.Gen.generate to generate a 3-element list, each element of which is a list of the kind you just created with generate1.
使用 QCheck.Gen.generate1 生成一个长度在 5 到 10 之间的列表,其元素是 0 到 100 之间的整数。然后使用 QCheck.Gen.generate 生成一个 3 元素的列表,每个元素这是您刚刚使用 generate1 创建的列表。

Then use QCheck.make to create an arbitrary that represents a list whose length is between 5 and 10, and whose elements are integers between 0 and 100. The type of your arbitrary should be int list QCheck.arbitrary.
然后使用 QCheck.make 创建一个任意值,表示长度在 5 到 10 之间的列表,其元素是 0 到 100 之间的整数。任意值的类型应该是 int list QCheck.arbitrary

Finally create and run a QCheck test that checks whether at least one element of an arbitrary list (of 5 to 10 elements, each between 0 and 100) is even. You’ll need to “upgrade” the is_even property to work on a list of integers rather than a single integer.
最后创建并运行 QCheck 测试,检查任意列表(包含 5 到 10 个元素,每个元素在 0 到 100 之间)中的至少一个元素是否为偶数。您需要“升级” is_even 属性才能处理整数列表而不是单个整数。

Each time you run the test, recall that it will generate 100 lists and check the property of them. If you run the test many times, you’ll likely see some successes and some failures.
每次运行测试时,请记住它将生成 100 个列表并检查它们的属性。如果您多次运行测试,您可能会看到一些成功和一些失败。


Exercise: qcheck odd divisor [★★★]
练习:检测奇除函数[★★★]

Here is a buggy function:
这是一个有缺陷的函数:

(** [odd_divisor x] is an odd divisor of [x].
    Requires: [x >= 0]. *)
let odd_divisor x =
  if x < 3 then 1 else
    let rec search y =
      if y >= x then y  (* exceeded upper bound *)
      else if x mod y = 0 then y  (* found a divisor! *)
      else search (y + 2) (* skip evens *)
    in search 3

Write a QCheck test to determine whether the output of that function (on a positive integer, per its precondition; hint: there is an arbitrary that generates positive integers) is both odd and is a divisor of the input. You will discover that there is a bug in the function. What is the smallest integer that triggers that bug?
编写一个 QCheck 测试来确定该函数的输出(根据其前提条件,为正整数;提示:存在生成正整数的任意值)是否既是奇数又是输入的除数。你会发现这个函数有一个 bug 。触发该错误的最小整数是多少?


Exercise: qcheck avg [★★★★]
练习:检测平均函数[★★★★]

Here is a buggy function:
这是一个有缺陷的函数:

(** [avg [x1; ...; xn]] is [(x1 + ... + xn) / n].
     Requires: the input list is not empty. *)
let avg lst =
  let rec loop (s, n) = function
    | [] -> (s, n)
    | [ h ] -> (s + h, n + 1)
    | h1 :: h2 :: t -> if h1 = h2 then loop (s + h1, n + 1) t
      else loop (s + h1 + h2, n + 2) t
  in
  let (s, n) = loop (0, 0) lst
  in float_of_int s /. float_of_int n

Write a QCheck test that detects the bug. For the property that you check, construct your own reference implementation of average—that is, a less optimized version of avg that is obviously correct.
编写一个 QCheck 测试来检测错误。对于您检查的属性,构建您自己的平均值参考实现,即明显正确的 avg 的优化程度较低的版本。


Exercise: exp [★★] 练习:求幂[★★]

Prove that exp x (m + n) = exp x m * exp x n, where
证明 exp x (m + n) = exp x m * exp x n ,其中

let rec exp x n =
  if n = 0 then 1 else x * exp x (n - 1)

Proceed by induction on m.
继续对 m 进行归纳。


Exercise: fibi [★★★] 练习:斐波那契[★★★]

Prove that forall n >= 1, fib n = fibi n (0, 1), where
证明 forall n >= 1, fib n = fibi n (0, 1) ,其中

let rec fib n =
  if n = 1 then 1
  else if n = 2 then 1
  else fib (n - 2) + fib (n - 1)

let rec fibi n (prev, curr) =
  if n = 1 then curr
  else fibi (n - 1) (curr, prev + curr)

Proceed by induction on n, rather than trying to apply the theorem about converting recursion into iteration.
n 进行归纳,而不是尝试应用有关将递归转换为迭代的定理。


Exercise: expsq [★★★] 练习:重复平方求幂[★★★]

Prove that expsq x n = exp x n, where
证明 expsq x n = exp x n ,其中

let rec expsq x n =
  if n = 0 then 1
  else if n = 1 then x
  else (if n mod 2 = 0 then 1 else x) * expsq (x * x) (n / 2)

Proceed by strong induction on n. Function expsq implements exponentiation by repeated squaring, which results in more efficient computation than exp.
n 进行强归纳。函数 expsq 通过重复平方实现求幂,这比 exp 的计算效率更高。


Exercise: expsq simplified [★★★]
练习:重复平方求幂简化[★★★]

Redo the preceding exercise, but with this simplified version of the function. The simplified version requires less code, but requires an additional recursive call.
重做前面的练习,但使用该函数的简化版本。简化版本需要更少的代码,但需要额外的递归调用。

let rec expsq' x n =
  if n = 0 then 1
  else (if n mod 2 = 0 then 1 else x) * expsq' (x * x) (n / 2)

Exercise: mult [★★] 练习:多[★★]

Prove that forall n, mult n Z = Z by induction on n, where:
通过对 n 进行归纳来证明 forall n, mult n Z = Z ,其中:

let rec mult a b =
  match a with
  | Z -> Z
  | S k -> plus b (mult k b)

Exercise: append nil [★★]
练习:追加 nil [★★]

Prove that forall lst, lst @ [] = lst by induction on lst.
通过对 lst 进行归纳证明 forall lst, lst @ [] = lst


Exercise: rev dist append [★★★]
练习:逆向分布追加[★★★]

Prove that reverse distributes over append, i.e., that forall lst1 lst2, rev (lst1 @ lst2) = rev lst2 @ rev lst1, where:
证明反向分布优于追加,即 forall lst1 lst2, rev (lst1 @ lst2) = rev lst2 @ rev lst1 ,其中:

let rec rev = function
  | [] -> []
  | h :: t -> rev t @ [h]

(That is, of course, an inefficient implementation of rev.) You will need to choose which list to induct over. You will need the previous exercise as a lemma, as well as the associativity of append, which was proved in the notes above.
(当然,这是 rev 的低效实现。)您将需要选择要归纳的列表。您将需要之前的练习作为引理,以及 append 的关联性,这已在上面的注释中得到证明。


Exercise: rev involutive [★★★]
练习:rev内卷[★★★]

Prove that reverse is an involution, i.e., that forall lst, rev (rev lst) = lst. Proceed by induction on lst. You will need the previous exercise as a lemma.
证明反向是对合,即 forall lst, rev (rev lst) = lst 。继续对 lst 进行归纳。您将需要之前的练习作为引理。


Exercise: reflect size [★★★]
练习:反映尺寸[★★★]

Prove that forall t, size (reflect t) = size t by induction on t, where:
通过对 t 进行归纳来证明 forall t, size (reflect t) = size t ,其中:

let rec size = function
  | Leaf -> 0
  | Node (l, v, r) -> 1 + size l + size r

Exercise: fold theorem 2 [★★★★]
练习:折叠定理2 [★★★★]

We proved that fold_left and fold_right yield the same results if their function argument is associative and commutative. But that doesn’t explain why these two implementations of concat yield the same results, because ( ^ ) is not commutative:
我们证明,如果 fold_leftfold_right 的函数参数是关联且可交换的,则它们会产生相同的结果。但这并不能解释为什么 concat 的这两个实现会产生相同的结果,因为 ( ^ ) 是不可交换的:

let concat_l lst = List.fold_left ( ^ ) "" lst
let concat_r lst = List.fold_right ( ^ ) lst ""

Formulate and prove a new theorem about when fold_left and fold_right yield the same results, under the relaxed assumption that their function argument is associative but not necessarily commutative. Hint: make a new assumption about the initial value of the accumulator.
制定并证明关于 fold_leftfold_right 何时产生相同结果的新定理,前提是它们的函数参数是关联的但不一定是可交换的。提示:对累加器的初始值做出新的假设。


Exercise: propositions [★★★★]
练习:命题[★★★★]

In propositional logic, we have atomic propositions, negation, conjunction, disjunction, and implication. For example, raining /\ snowing /\ cold is a proposition stating that it is simultaneously raining and snowing and cold (a weather condition known as Ithacating).
在命题逻辑中,我们有原子命题、否定、合取、析取和蕴涵。例如, raining /\ snowing /\ cold 是一个命题,表示同时下雨、下雪和寒冷(称为 Ithacating 的天气条件)。

Define an OCaml type to represent propositions. Then state the induction principle for that type.
定义 OCaml 类型来表示命题。然后陈述该类型的归纳原理。


Exercise: list spec [★★★]
练习:序列的规格[★★★]

Design an OCaml interface for lists that has nil, cons, append, and length operations. Design the equational specification. Hint: the equations will look strikingly like the OCaml implementations of @ and List.length.
为具有 nilconsappendlength 操作的列表设计 OCaml 接口。设计等式规范。提示:方程看起来与 @List.length 的 OCaml 实现非常相似。


Exercise: bag spec [★★★★]
练习:包的规格[★★★★]

A bag or multiset is like a blend of a list and a set: like a set, order does not matter; like a list, elements may occur more than once. The number of times an element occurs is its multiplicity. An element that does not occur in the bag has multiplicity 0. Here is an OCaml signature for bags:
包或多集就像列表和集合的混合:就像集合一样,顺序并不重要;与列表一样,元素可能会出现多次。一个元素出现的次数就是它的重数。包中未出现的元素的重数为 0。这是包的 OCaml 签名:

module type Bag = sig
  type 'a t
  val empty : 'a t
  val is_empty : 'a t -> bool
  val insert : 'a -> 'a t -> 'a t
  val mult : 'a -> 'a t -> int
  val remove : 'a -> 'a t -> 'a t
end

Categorize the operations in the Bag interface as generators, manipulators, or queries. Then design an equational specification for bags. For the remove operation, your specification should cause at most one occurrence of an element to be removed. That is, the multiplicity of that value should decrease by at most one.
Bag 接口中的操作分类为生成器、操纵器或查询。然后设计袋子的等式规格。对于 remove 操作,您的规范应该导致最多出现一个元素被删除。也就是说,该值的重数最多应减少一。

7. Mutability 7. 可变性 ¶

OCaml is not a pure language: it does admit side effects. We have seen that already with I/O, especially printing. But up till now we have limited ourself to the subset of the language that is immutable: values could not change.
OCaml 不是一种纯粹的语言:它确实存在副作用。我们已经在 I/O 中看到了这一点,尤其是打印。但到目前为止,我们将自己限制在不可变语言的子集上:值不能改变。

Mutability is neither good nor bad. It enables new functionality that we couldn’t implement (at least not easily) before, and it enables us to create certain data structures that are asymptotically more efficient than their purely functional analogues. But mutability does make code more difficult to reason about, hence it is a source of many faults in code. One reason for that might be that humans are not good at thinking about change. With immutable values, we’re guaranteed that any fact we might establish about them can never change. But with mutable values, that’s no longer true. “Change is hard,” as they say.
可变性既不好也不坏。它实现了我们以前无法实现(至少不容易实现)的新功能,并且使我们能够创建某些数据结构,这些数据结构比纯函数式类似物渐近更高效。但可变性确实使代码更难以推理,因此它是代码中许多错误的根源。原因之一可能是人类不善于思考变化。有了不可变的价值观,我们就可以保证我们建立的关于它们的任何事实都永远不会改变。但随着价值观的变化,情况就不再如此了。正如他们所说,“改变是困难的”。

In this short chapter we’ll cover the few mutable features of OCaml we’ve omitted so far, and we’ll use them for some simple data structures. The real win, though, will come in the next chapter, where we put the features to more complicated uses.
在这一简短的章节中,我们将介绍迄今为止省略的 OCaml 的一些可变功能,并将它们用于一些简单的数据结构。不过,真正的胜利将在下一章出现,我们将把这些功能用于更复杂的用途。

7.1. Refs 7.1. 引用 ¶

A ref is like a pointer or reference in an imperative language. It is a location in memory whose contents may change. Refs are also called ref cells, the idea being that there’s a cell in memory that can change.
ref 就像命令式语言中的指针或引用。它是内存中的一个位置,其内容可能会发生变化。引用也称为引用单元,其含义是内存中有一个可以更改的单元。

Here’s an example of creating a ref, getting the value from inside it, changing its contents, and observing the changed contents:
下面是创建 ref、从其内部获取值、更改其内容并观察更改的内容的示例:

let x = ref 0;;
val x : int ref = {contents = 0}
!x;;
- : int = 0
x := 1;;
- : unit = ()
!x;;
- : int = 1

The first phrase, let x = ref 0, creates a reference using the ref keyword. That’s a location in memory whose contents are initialized to 0. Think of the location itself as being an address—for example, 0x3110bae0—even though there’s no way to write down such an address in an OCaml program. The keyword ref is what causes the memory location to be allocated and initialized.
第一个短语 let x = ref 0 使用 ref 关键字创建引用。这是内存中的一个位置,其内容被初始化为 0 。将该位置本身视为一个地址(例如 0x3110bae0),尽管无法在 OCaml 程序中写下这样的地址。关键字 ref 导致内存位置被分配和初始化。

The first part of the response from OCaml, val x : int ref, indicates that x is a variable whose type is int ref. We have a new type constructor here. Much like list and option are type constructors, so is ref. A t ref, for any type t, is a reference to a memory location that is guaranteed to contain a value of type t. As usual we should read a type from right to left: t ref means a reference to a t. The second part of the response shows us the contents of the memory location. Indeed, the contents have been initialized to 0.
OCaml 响应的第一部分 val x : int ref 指示 x 是一个类型为 int ref 的变量。我们这里有一个新的类型构造函数。就像 listoption 是类型构造函数一样, ref 也是如此。对于任何类型 t 来说, t ref 是对保证包含 t 类型值的内存位置的引用。像往常一样,我们应该从右到左读取类型: t ref 表示对 t 的引用。响应的第二部分向我们展示了内存位置的内容。事实上,内容已被初始化为 0

The second phrase, !x, dereferences x and returns the contents of the memory location. Note that ! is the dereference operator in OCaml, not Boolean negation.
第二个短语 !x 取消引用 x 并返回内存位置的内容。请注意, ! 是 OCaml 中的取消引用运算符,而不是布尔否定。

The third phrase, x := 1, is an assignment. It mutates the contents x to be 1. Note that x itself still points to the same location (i.e., address) in memory. Memory is mutable; variable bindings are not. What changes is the contents. The response from OCaml is simply (), meaning that the assignment took place—much like printing functions return () to indicate that the printing did happen.
第三个短语 x := 1 是一个赋值。它将内容 x 更改为 1 。请注意, x 本身仍然指向内存中的同一位置(即地址)。记忆是可变的;变量绑定则不然。改变的是内容。 OCaml 的响应只是 () ,这意味着分配已发生 - 很像打印函数返回 () 来指示打印确实发生。

The fourth phrase, !x again dereferences x to demonstrate that the contents of the memory location did indeed change.
第四个短语 !x 再次取消引用 x 以证明内存位置的内容确实发生了变化。

7.1.1. Aliasing 7.1.1. 别名 ¶

Now that we have refs, we have aliasing: two refs could point to the same memory location, hence updating through one causes the other to also be updated. For example,
现在我们有了引用,我们就有了别名:两个引用可以指向相同的内存位置,因此通过一个引用进行更新会导致另一个引用也被更新。例如,

let x = ref 42;;
let y = ref 42;;
let z = x;;
x := 43;;
let w = !y + !z;;
val x : int ref = {contents = 42}
val y : int ref = {contents = 42}
val z : int ref = {contents = 42}
- : unit = ()
val w : int = 85

The result of executing that code is that w is bound to 85, because let z = x causes z and x to become aliases, hence updating x to be 43 also causes z to be 43.
执行该代码的结果是 w 绑定到 85 ,因为 let z = x 导致 zx 成为别名,因此将 x 更新为 43 也会导致 z 变为 43

7.1.2. Syntax and Semantics
7.1.2. 语法和语义 ¶

The semantics of refs is based on locations in memory. Locations are values that can be passed to and returned from functions. But unlike other values (e.g., integers, variants), there is no way to directly write a location in an OCaml program. That’s different than languages like C, in which programmers can directly write memory addresses and do arithmetic on pointers. C programmers want that kind of low-level access to do things like interfacing with hardware and building operating systems. Higher-level programmers are willing to forego it to get memory safety. That’s a hard term to define, but according to Hicks 2014 it intuitively means that
refs 的语义基于内存中的位置。位置是可以传递给函数并从函数返回的值。但与其他值(例如整数、变体)不同,无法直接在 OCaml 程序中写入位置。这与 C 等语言不同,在 C 语言中,程序员可以直接写入内存地址并对指针进行算术运算。 C 程序员希望通过这种低级访问来完成诸如与硬件交互和构建操作系统之类的事情。更高级别的程序员愿意放弃它以获得内存安全。这是一个很难定义的术语,但根据 Hicks 2014,它直观地意味着

  • pointers are only created in a safe way that defines their legal memory region,
    指针仅以定义其合法内存区域的安全方式创建,

  • pointers can only be dereferenced if they point to their allotted memory region,
    仅当指针指向其分配的内存区域时才可以取消引用,

  • that region is (still) defined.
    该区域(仍然)是确定的。

Syntax.

  • Ref creation: ref e 引用创建: ref e

  • Ref assignment: e1 := e2 引用分配: e1 := e2

  • Dereference: !e 取消引用: !e

Dynamic semantics. 动态语义。

  • To evaluate ref e, 要评估 ref e

    • Evaluate e to a value v
      e 计算为值 v

    • Allocate a new location loc in memory to hold v
      在内存中分配一个新位置 loc 来保存 v

    • Store v in loc
      v 存储在 loc

    • Return loc 返回 loc

  • To evaluate e1 := e2, 要评估 e1 := e2

    • Evaluate e2 to a value v, and e1 to a location loc.
      e2 计算为值 v ,将 e1 计算为位置 loc

    • Store v in loc.
      v 存储在 loc 中。

    • Return (), i.e., unit.
      返回 () ,即单位。

  • To evaluate !e, 要评估 !e

    • Evaluate e to a location loc.
      e 评估为位置 loc

    • Return the contents of loc.
      返回 loc 的内容。

Static semantics. 静态语义。

We have a new type constructor, ref, such that t ref is a type for any type t. Note that the ref keyword is used in two ways: as a type constructor, and as an expression that constructs refs.
我们有一个新的类型构造函数 ref ,这样 t ref 是任何类型 t 的类型。请注意, ref 关键字有两种使用方式:作为类型构造函数,以及作为构造引用的表达式。

  • ref e : t ref if e : t.
    ref e : t refe : t

  • e1 := e2 : unit if e1 : t ref and e2 : t.
    e1 := e2 : unite1 : t refe2 : t

  • !e : t if e : t ref.
    !e : te : t ref

7.1.3. Sequencing of Effects
7.1.3. 对影响的排序 ¶

The semicolon operator is used to sequence effects, such as mutating refs. We’ve seen semicolon occur previously with printing. Now that we’re studying mutability, it’s time to treat it formally.
分号运算符用于对影响进行排序,例如改变引用。我们之前已经在打印中看到过分号的出现。现在我们正在研究可变性,是时候正式对待它了。

  • Syntax: e1; e2 语法: e1; e2

  • Dynamic semantics: To evaluate e1; e2,
    动态语义:要评估 e1; e2

    • First evaluate e1 to a value v1.
      首先将 e1 评估为值 v1

    • Then evaluate e2 to a value v2.
      然后将 e2 评估为值 v2

    • Return v2. (v1 is not used at all.)
      返回 v2 。 ( v1 根本没有使用。)

    • If there are multiple expressions in a sequence, e.g., e1; e2; ...; en, then evaluate each one in order from left to right, returning only vn.
      如果序列中有多个表达式,例如 e1; e2; ...; en ,则按从左到右的顺序计算每个表达式,仅返回 vn

  • Static semantics: e1; e2 : t if e1 : unit and e2 : t. Similarly, e1; e2; ...; en : t if e1 : unit, e2 : unit, … (i.e., all expressions except en have type unit), and en : t.
    静态语义: e1; e2 : te1 : unite2 : t 。类似地, e1; e2; ...; en : te1 : unite2 : unit 、 ... (即,除 en 之外的所有表达式都具有类型 unit )和 en : t

The typing rule for semicolon is designed to prevent programmer mistakes. For example, a programmer who writes 2+3; 7 probably didn’t mean to: there’s no reason to evaluate 2+3 then throw away the result and instead return 7. The compiler will give you a warning if you violate this particular typing rule.
分号的输入规则旨在防止程序员犯错误。例如,编写 2+3; 7 的程序员可能无意:没有理由评估 2+3 然后丢弃结果并返回 7 。如果您违反此特定的键入规则,编译器将向您发出警告。

To get rid of the warning (if you’re sure that’s what you need to do), there’s a function ignore : 'a -> unit in the standard library. Using it, ignore(2+3); 7 will compile without a warning. Of course, you could code up ignore yourself: let ignore _ = ().
为了消除警告(如果您确定这就是您需要做的),标准库中有一个函数 ignore : 'a -> unit 。使用它, ignore(2+3); 7 将在没有警告的情况下进行编译。当然,您可以自己编写 ignore 代码: let ignore _ = ()

7.1.4. Example: Mutable Counter
7.1.4. 示例:可变计数器 ¶

Here is code that implements a counter. Every time next_val is called, it returns one more than the previous time.
这是实现计数器的代码。每次调用 next_val 时,它都会比前一次多返回 1。

let counter = ref 0

let next_val =
  fun () ->
    counter := !counter + 1;
    !counter
val counter : int ref = {contents = 0}
val next_val : unit -> int = <fun>
next_val ()
- : int = 1
next_val ()
- : int = 2
next_val ()
- : int = 3

In the implementation of next_val, there are two expressions separated by semi-colon. The first expression, counter := !counter + 1, is an assignment that increments counter by 1. The second expression, !counter, returns the newly incremented contents of counter.
next_val 的实现中,有两个用分号分隔的表达式。第一个表达式 counter := !counter + 1 是一个将 counter 递增 1 的赋值。第二个表达式 !counter 返回 counter 新递增完了的内容。

The next_val function is unusual in that every time we call it, it returns a different value. That’s quite different than any of the functions we’ve implemented ourselves so far, which have always been deterministic: for a given input, they always produced the same output. On the other hand, we’ve seen some library functions that are nondeterministic, for example, functions in the Random module, and Stdlib.read_line. It’s no coincidence that those happen to be implemented using mutable features.
next_val 函数很不寻常,因为每次调用它时,它都会返回不同的值。这与我们迄今为止实现的任何函数都有很大不同,这些函数始终是确定性的:对于给定的输入,它们总是产生相同的输出。另一方面,我们看到了一些不确定的库函数,例如 Random 模块中的函数和 Stdlib.read_line 。这些恰好是使用可变功能来实现的,这并非巧合。

We could improve our counter in a couple ways. First, there is a library function incr : int ref -> unit that increments an int ref by 1. Thus it is like the ++ operator that is familiar from many languages in the C family. Using it, we could write incr counter instead of counter := !counter + 1. (There’s also a decr function that decrements by 1.)
我们可以通过几种方式改进我们的计数器。首先,有一个库函数 incr : int ref -> unitint ref 加 1。因此,它就像 C 家族中许多语言所熟悉的 ++ 运算符一样。使用它,我们可以编写 incr counter 而不是 counter := !counter + 1 。 (还有一个 decr 函数减1。)

Second, the way we coded the counter currently exposes the counter variable to the outside world. Maybe we’re prefer to hide it so that clients of next_val can’t directly change it. We could do so by nesting counter inside the scope of next_val:
其次,我们目前对计数器进行编码的方式将 counter 变量暴露给外界。也许我们更愿意隐藏它,以便 next_val 的客户端无法直接更改它。我们可以通过将 counter 嵌套在 next_val 范围内来实现:

let next_val =
  let counter = ref 0 in
  fun () ->
    incr counter;
    !counter
val next_val : unit -> int = <fun>

Now counter is in scope inside of next_val, but not accessible outside that scope.
现在 counter 位于 next_val 内部的范围内,但在该范围之外无法访问。

When we gave the dynamic semantics of let expressions before, we talked about substitution. One way to think about the definition of next_val is as follows.
之前我们给出 let 表达式的动态语义时,我们谈到了替换。考虑 next_val 定义的一种方法如下。

  • First, the expression ref 0 is evaluated. That returns a location loc, which is an address in memory. The contents of that address are initialized to 0.
    首先,计算表达式 ref 0 。它返回一个位置 loc ,它是内存中的地址。该地址的内容被初始化为 0

  • Second, everywhere in the body of the let expression that counter occurs, we substitute for it that location. So we get:
    其次,在 let 表达式主体中出现 counter 的任何地方,我们都将其替换为该位置。所以我们得到:

    fun () -> incr loc; !loc
    
  • Third, that anonymous function is bound to next_val.
    第三,该匿名函数绑定到 next_val

So any time next_val is called, it increments and returns the contents of that one memory location loc.
因此,每次调用 next_val 时,它都会递增并返回该内存位置 loc 的内容。

Now imagine that we instead had written the following (broken) code:
现在想象一下我们编写了以下(损坏的)代码:

let next_val_broken = fun () ->
  let counter = ref 0 in
  incr counter;
  !counter
val next_val_broken : unit -> int = <fun>

It’s only a little different: the binding of counter occurs after the fun () -> instead of before. But it makes a huge difference:
只是有一点不同: counter 的绑定发生在 fun () -> 之后,而不是之前。但这有很大的不同:

next_val_broken ();;
next_val_broken ();;
next_val_broken ();;
- : int = 1
- : int = 1
- : int = 1

Every time we call next_val_broken, it returns 1: we no longer have a counter. What’s going wrong here?
每次我们调用 next_val_broken 时,它都会返回 1 :我们不再有计数器。这里出了什么问题?

The problem is that every time next_val_broken is called, the first thing it does is to evaluate ref 0 to a new location that is initialized to 0. That location is then incremented to 1, and 1 is returned. Every call to next_val_broken is thus allocating a new ref cell, whereas next_val allocates just one new ref cell.
问题是,每次调用 next_val_broken 时,它所做的第一件事就是将 ref 0 评估为初始化为 0 的新位置。然后该位置递增到 1 ,并返回 1 。因此,每次调用 next_val_broken 都会分配一个新的引用单元,而 next_val 仅分配一个新的引用单元。

7.1.5. Example: Pointers
7.1.5. 示例:指针 ¶

In languages like C, pointers combine two features: they can be null, and they can be changed. (Java has a similar construct with object references, but that term is confusing in our OCaml context since “reference” currently means a ref cell. So we’ll stick with the word “pointer”.) Let’s code up pointers using OCaml ref cells.
在 C 等语言中,指针结合了两个特性:它们可以为 null,并且可以更改。 (Java 有一个类似的对象引用构造,但该术语在我们的 OCaml 上下文中令人困惑,因为“引用”当前表示引用单元。因此我们将继续使用“指针”一词。)让我们使用 OCaml 引用单元来编写指针。

type 'a pointer = 'a ref option
type 'a pointer = 'a ref option

As usual, read that type right to left. The option part of it encodes the fact that a pointer might be null. We’re using None to represent that possibility.
像往常一样,从右到左阅读该类型。它的 option 部分编码了指针可能为空的事实。我们使用 None 来表示这种可能性。

let null : 'a pointer = None
val null : 'a pointer = None

The ref part of the type encodes the fact that the contents are mutable. We can create a helper function to allocate and initialize the contents of a new pointer:
类型的 ref 部分编码了内容是可变的这一事实。我们可以创建一个辅助函数来分配和初始化新指针的内容:

let malloc (x : 'a) : 'a pointer = Some (ref x)
val malloc : 'a -> 'a pointer = <fun>

Now we could create a pointer to any value we like:
现在我们可以创建一个指向我们喜欢的任何值的指针:

let p = malloc 42
val p : int pointer = Some {contents = 42}

Dereferencing a pointer is the * prefix operator in C. It returns the contents of the pointer, and raises an exception if the pointer is null:
取消引用指针是 C 中的 * 前缀运算符。它返回指针的内容,如果指针为 null,则引发异常:

exception Segfault

let deref (ptr : 'a pointer) : 'a =
  match ptr with None -> raise Segfault | Some r -> !r
exception Segfault
val deref : 'a pointer -> 'a = <fun>
deref p
- : int = 42
deref null
Exception: Segfault.
Raised at deref in file "[17]", line 4, characters 25-39
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

We could even introduce our own OCaml operator for dereference. We have to put ~ in front of it to make it parse as a prefix operator, though.
我们甚至可以引入我们自己的 OCaml 运算符来取消引用。不过,我们必须将 ~ 放在它前面才能将其解析为前缀运算符。

let ( ~* ) = deref;;
~*p
val ( ~* ) : 'a pointer -> 'a = <fun>
- : int = 42

In C, an assignment through a pointer is written *p = x. That changes the memory to which p points, making it contain x. We can code up that operator as follows:
在 C 语言中,通过指针进行赋值被写为 *p = x 。这会更改 p 指向的内存,使其包含 x 。我们可以按如下方式对该运算符进行编码:

let assign (ptr : 'a pointer) (x : 'a) : unit =
  match ptr with None -> raise Segfault | Some r -> r := x
val assign : 'a pointer -> 'a -> unit = <fun>
assign p 2;
deref p
- : int = 2
assign null 0
Exception: Segfault.
Raised at assign in file "[21]", line 2, characters 25-39
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

Again, we could introduce our own OCaml operator for that, though it’s hard to pick a good symbol involving * and = that won’t be misunderstood as involving multiplication:
同样,我们可以为此引入我们自己的 OCaml 运算符,尽管很难选择一个涉及 *= 的好的符号,并且不会被误解为涉及乘法:

let ( =* ) = assign;;
p =* 3;;
~*p
val ( =* ) : 'a pointer -> 'a -> unit = <fun>
- : unit = ()
- : int = 3

The one thing we can’t do is treat a pointer as an integer. C allows that, including taking the address of a variable, which enables pointer arithmetic. That’s great for efficiency, but also terrible because it leads to all kinds of program errors and security vulnerabilities.
我们不能做的一件事就是将指针视为整数。 C 允许这样做,包括获取变量的地址,从而实现指针算术。这对于提高效率很有帮助,但也很糟糕,因为它会导致各种程序错误和安全漏洞。

Evil Secret 邪恶的秘密

Okay that wasn’t actually true what we just said, but this is dangerous knowledge that you really shouldn’t even read. There is an undocumented function Obj.magic
好吧,我们刚才所说的实际上并不正确,但这是危险的知识,你真的不应该阅读。有一个未记录的函数 Obj.magic 我们可以使用它来获取引用的内存地址:
that we could use to get a memory address of a ref:

let address (ptr : 'a pointer) : int =
  match ptr with None -> 0 | Some r -> Obj.magic r

let ( ~& ) = address

But you have to promise to never, ever use that function yourself, because it completely circumvents the safety of the OCaml type system. All bets are off if you do.
但您必须保证自己永远不会使用该函数,因为它完全规避了 OCaml 类型系统的安全性。如果你这样做了,所有的赌注都会被取消。

None of this pointer encoding is part of the OCaml standard library, because you don’t need it. You can always use refs and options yourself as you need to. Coding as we just did above is not particularly idiomatic. The reason we did it was to illustrate the relationship between OCaml refs and C pointers (equivalently, Java references).
这些指针编码都不属于 OCaml 标准库的一部分,因为您不需要它。您可以根据需要自行使用 refs 和 options。我们上面所做的编码并不是特别惯用。我们这样做的原因是为了说明 OCaml 引用和 C 指针(相当于 Java 引用)之间的关系。

7.1.6. Example: Recursion Without Rec
7.1.6. 示例:没有 Rec 的递归 ¶

Here’s a neat trick that’s possible with refs: we can build recursive functions without ever using the keyword rec. Suppose we want to define a recursive function such as fact, which we would normally write as follows:
这是一个可以通过 refs 实现的巧妙技巧:我们可以构建递归函数,而无需使用关键字 rec 。假设我们要定义一个递归函数,例如 fact ,我们通常会编写如下:

let rec fact_rec n = if n = 0 then 1 else n * fact_rec (n - 1)
val fact_rec : int -> int = <fun>

We want to define that function without using rec. We can begin by defining a ref to an obviously incorrect version of the function:
我们希望在不使用 rec 的情况下定义该函数。我们可以首先定义对函数的明显不正确版本的引用:

let fact0 = ref (fun x -> x + 0)
val fact0 : (int -> int) ref = {contents = <fun>}

The way in which fact0 is incorrect is actually irrelevant. We just need it to have the right type. We could just as well have used fun x -> x instead of fun x -> x + 0.
fact0 的错误方式实际上是无关紧要的。我们只需要它有正确的类型。我们也可以使用 fun x -> x 而不是 fun x -> x + 0

At this point, fact0 clearly doesn’t compute the factorial function. For example, 5! ought to be 120, but that’s not what fact0 computes:
此时, fact0 显然不计算阶乘函数。例如, 5! 应该是 120,但这不是 fact0 的计算结果:

!fact0 5
- : int = 5

Next, we write fact as usual, but without rec. At the place where we need to make the recursive call, we instead invoke the function stored inside fact0:
接下来,我们照常编写 fact ,但没有 rec 。在需要进行递归调用的地方,我们改为调用 fact0 中存储的函数:

let fact n = if n = 0 then 1 else n * !fact0 (n - 1)
val fact : int -> int = <fun>

Now fact does actually get the right answer for 0, but not for 5:
现在 fact 实际上得到了 0 的正确答案,但没有得到 5 的正确答案:

fact 0;;
fact 5;;
- : int = 1
- : int = 20

The reason it’s not right for 5 is that the recursive call isn’t actually to the right function. We want the recursive call to go to fact, not to fact0. So here’s the trick: we mutate fact0 to point to fact:
5 不正确的原因是递归调用实际上并不是正确的函数。我们希望递归调用转到 fact ,而不是 fact0 。所以这就是技巧:我们将 fact0 突变为指向 fact

fact0 := fact
- : unit = ()

Now when fact makes its recursive call and dereferences fact0, it gets back itself! That makes the computation correct:
现在,当 fact 进行递归调用并取消引用 fact0 时,它会返回自身!这使得计算正确:

fact 5
- : int = 120

Abstracting a little, here’s what we did. We started with a function that is recursive:
抽象一点,这就是我们所做的。我们从一个递归函数开始:

let rec f x = ... f y ...

We rewrote it as follows:
我们将其重写如下:

let f0 = ref (fun x -> x)

let f x = ... !f0 y ...

f0 := f

Now f will compute the same result as it did in the version where we defined it with rec.
现在 f 将计算与我们使用 rec 定义的版本中相同的结果。

What’s happening here is sometimes called “tying the recursive knot”: we update the reference to f0 to point to f, such that when f dereferences f0, it gets itself back. The initial function to which we made f0 point (in this case the identity function) doesn’t really matter; it’s just there as a placeholder until we tie the knot.
这里发生的事情有时被称为“打递归结”:我们更新对 f0 的引用以指向 f ,这样当 f 取消引用 f0 ,它自己回来了。我们 f0 指向的初始函数(在本例中为恒等函数)并不重要;它只是作为占位符存在,直到我们结婚为止。

7.1.7. Weak Type Variables
7.1.7. 弱类型变量 ¶

Perhaps you have already tried using the identity function to define fact0, as we mentioned above. If so, you will have encountered this rather puzzling output:
也许您已经尝试过使用恒等函数来定义 fact0 ,正如我们上面提到的。如果是这样,您将遇到以下令人费解的输出:

let fact0 = ref (fun x -> x)
val fact0 : ('_weak1 -> '_weak1) ref = {contents = <fun>}

What is this strange type for the identity function, '_weak1 -> '_weak1? Why isn’t it the usual 'a -> 'a?
恒等函数 '_weak1 -> '_weak1 的这种奇怪类型是什么?为什么不是通常的 'a -> 'a

The answer has to do with a particularly tricky interaction between polymorphism and mutability. In a later chapter on interpreters, we’ll learn how type inference works, and at that point we’ll be able to explain the problem in detail. In short, allowing the type 'a -> 'a for that ref would lead to the possibility of programs that crash at run time because of type errors.
答案与多态性和可变性之间特别棘手的相互作用有关。在后面关于解释器的章节中,我们将学习类型推断的工作原理,届时我们将能够详细解释该问题。简而言之,允许该引用使用类型 'a -> 'a 可能会导致程序由于类型错误而在运行时崩溃。

For now, think about it this way: although the value stored in a ref cell is permitted to change, the type of that value is not. And if OCaml gave ref (fun x -> x) the type ('a -> 'a) ref, then that cell could first store fun x -> x + 1 : int -> int but later store fun x -> s ^ "!" : string -> string. That would be the kind of change in type that is not allowed.
现在,这样考虑:虽然允许更改存储在引用单元格中的值,但该值的类型却不允许更改。如果 OCaml 为 ref (fun x -> x) 指定类型 ('a -> 'a) ref ,那么该单元可以首先存储 fun x -> x + 1 : int -> int 但随后存储 fun x -> s ^ "!" : string -> string 。这是不允许的类型更改。

So OCaml uses weak type variables to stand for unknown but not polymorphic types. These variables always start with _weak. Essentially, type inference for these is just not finished yet. Once you give OCaml enough information, it will finish type inference and replace the weak type variable with the actual type:
因此 OCaml 使用弱类型变量来代表未知但非多态类型。这些变量始终以 _weak 开头。本质上,这些的类型推断还没有完成。一旦你给 OCaml 足够的信息,它将完成类型推断并用实际类型替换弱类型变量:

!fact0
- : '_weak1 -> '_weak1 = <fun>
!fact0 1
- : int = 1
!fact0
- : int -> int = <fun>

After the application of !fact0 to 1, OCaml now knows that the function is meant to have type int -> int. So from then on, that’s the only type at which it can be used. It can’t, for example, be applied to a string.
!fact0 应用到 1 后,OCaml 现在知道该函数的类型为 int -> int 。所以从那时起,这是它唯一可以使用的类型。例如,它不能应用于字符串。

!fact0 "camel"
File "[36]", line 1, characters 7-14:
1 | !fact0 "camel"
           ^^^^^^^
Error: This expression has type string but an expression was expected of type
         int

If you would like to learn more about weak type variables right now, take a look at Section 2 of Relaxing the value restriction by Jacques Garrigue, or this section of the OCaml manual.
如果您现在想了解有关弱类型变量的更多信息,请查看 Jacques Garrigue 的放宽值限制的第 2 部分,或 OCaml 手册的本部分。

7.1.8. Physical Equality
7.1.8. 物理相等 ¶

OCaml has two equality operators, physical equality and structural equality. The documentation of Stdlib.(==) explains physical equality:
OCaml 有两个相等运算符:物理相等和结构相等。 Stdlib.(==) 的文档解释了物理平等:

e1 == e2 tests for physical equality of e1 and e2. On mutable types such as references, arrays, byte sequences, records with mutable fields and objects with mutable instance variables, e1 == e2 is true if and only if physical modification of e1 also affects e2. On non-mutable types, the behavior of ( == ) is implementation-dependent; however, it is guaranteed that e1 == e2 implies compare e1 e2 = 0.
e1 == e2 测试 e1e2 的物理相等性。对于可变类型,例如引用、数组、字节序列、具有可变字段的记录以及具有可变实例变量的对象,当且仅当对 e1 的影响也会影响 e2 时, e1 == e2 才为 true 。对于非可变类型, ( == ) 的行为依赖于实现;但是,可以保证 e1 == e2 暗示 compare e1 e2 = 0

One interpretation could be that == should be used only when comparing refs (and other mutable data types) to see whether they point to the same location in memory. Otherwise, don’t use ==.
一种解释可能是,仅在比较引用(和其他可变数据类型)以查看它们是否指向内存中的同一位置时才应使用 == 。否则,不要使用 ==

Structural equality is also explained in the documentation of Stdlib.(=):
Stdlib.(=) 的文档中也解释了结构平等:

e1 = e2 tests for structural equality of e1 and e2. Mutable structures (e.g. references and arrays) are equal if and only if their current contents are structurally equal, even if the two mutable objects are not the same physical object. Equality between functional values raises Invalid_argument. Equality between cyclic data structures may not terminate.
e1 = e2 测试 e1e2 的结构相等性。可变结构(例如引用和数组)当且仅当它们当前内容在结构上相等时才相等,即使两个可变对象不是同一个物理对象。函数值之间的相等会引发 Invalid_argument 。循环数据结构之间的相等可能不会终止。

Structural equality is usually what you want to test. For refs, it checks whether the contents of the memory location are equal, regardless of whether they are the same location.
结构平等通常是您想要测试的。对于refs,它检查内存位置的内容是否相等,无论它们是否是同一位置。

The negation of physical equality is !=, and the negation of structural equality is <>. This can be hard to remember.
物理平等的否定是 != ,结构平等的否定是 <> 。这可能很难记住。

Here are some examples involving equality and refs to illustrate the difference between structural equality (=) and physical equality (==):
以下是一些涉及相等和引用的示例,以说明结构相等( = )和物理相等( == )之间的区别:

let r1 = ref 42
let r2 = ref 42
val r1 : int ref = {contents = 42}
val r2 : int ref = {contents = 42}

A ref is physically equal to itself, but not to another ref that is a different location in memory:
引用在物理上等于其自身,但不等于内存中不同位置的另一个引用:

r1 == r1
- : bool = true
r1 == r2
- : bool = false
r1 != r2
- : bool = true

Two refs that are at different locations in memory but store structurally equal values are themselves structurally equal:
位于内存中不同位置但存储结构相同值的两个引用本身结构相同:

r1 = r1
- : bool = true
r1 = r2
- : bool = true
r1 <> r2
- : bool = false

Two refs that store structurally unequal values are themselves structurally unequal:
存储结构上不相等值的两个引用本身结构上也不相等:

ref 42 <> ref 43
- : bool = true

7.1.9. Example: Singly-linked Lists
7.1.9. 示例:单链表 ¶

OCaml’s built-in singly-linked lists are functional, not imperative. But we can code up imperative singly-linked lists, of course, with refs. (We could also use the pointers we invented above, but that only makes the code more complicated.)
OCaml 的内置单链表是功能性的,而不是命令性的。但是我们当然可以使用 refs 编写命令式单链表。 (我们也可以使用上面发明的指针,但这只会使代码更加复杂。)

We start by defining a type 'a node for nodes of a list that contains values of type 'a. The next field of a node is itself another list.
我们首先为包含类型 'a 值的列表的节点定义类型 'a node 。节点的 next 字段本身就是另一个列表。

(** An ['a node] is a node of a mutable singly-linked list. It contains a value
    of type ['a] and a link to the [next] node. *)
type 'a node = { next : 'a mlist; value : 'a }

(** An ['a mlist] is a mutable singly-linked list with elements of type ['a].
    The [option] represents the possibility that the list is empty.
    RI: The list does not contain any cycles. *)
and 'a mlist = 'a node option ref
type 'a node = { next : 'a mlist; value : 'a; }
and 'a mlist = 'a node option ref

To create an empty list, we simply return a ref to None:
要创建一个空列表,我们只需返回一个对 None 的引用:

(** [empty ()] is an empty singly-linked list. *)
let empty () : 'a mlist = ref None
val empty : unit -> 'a mlist = <fun>

Note the type of empty: instead of being a value, it is now a function. This is typical of functions that create mutable data structures. At the end of this section, we’ll return to why empty has to be a function.
请注意 empty 的类型:它现在不再是一个值,而是一个函数。这是创建可变数据结构的典型函数。在本节的末尾,我们将回到为什么 empty 必须是一个函数。

Inserting a new first element just requires creating a new node, linking from it to the original list, and mutating the list:
插入新的第一个元素只需要创建一个新节点,从它链接到原始列表,然后改变列表:

(** [insert_first lst v] mutates mlist [lst] by inserting value [v] as the
    first value in the list. *)
let insert_first (lst : 'a mlist) (v : 'a) : unit =
  lst := Some { next = ref !lst; value = v }
val insert_first : 'a mlist -> 'a -> unit = <fun>

Again, note the type of insert_first. Rather than returning an 'a mlist, it returns unit. This again is typical of functions that modify mutable data structures.
再次注意 insert_first 的类型。它不返回 'a mlist ,而是返回 unit 。这又是修改可变数据结构的函数的典型特征。

In both empty and insert_first, the use of unit makes the functions more like their equivalents in an imperative language. The constructor for an empty list in Java, for example, might not take any arguments (which is equivalent to taking unit). And the insert_first operation for a Java linked list might return void, which is equivalent to returning unit.
emptyinsert_first 中,使用 unit 使函数更像命令式语言中的等效函数。例如,Java 中空列表的构造函数可能不接受任何参数(相当于接受 unit )。 Java 链表的 insert_first 操作可能会返回 void ,这相当于返回 unit

Finally, here’s a conversion function from our new mutable lists to OCaml’s built-in lists:
最后,这是从我们的新可变列表到 OCaml 内置列表的转换函数:

(** [to_list lst] is an OCaml list containing the same values as [lst]
    in the same order. Not tail recursive. *)
let rec to_list (lst : 'a mlist) : 'a list =
  match !lst with None -> [] | Some { next; value } -> value :: to_list next
val to_list : 'a mlist -> 'a list = <fun>

Now we can see mutability in action:
现在我们可以看到实际的可变性:

let lst0 = empty ();;
let lst1 = lst0;;
insert_first lst0 1;;
to_list lst1;;
val lst0 : '_weak2 mlist = {contents = None}
val lst1 : '_weak2 mlist = {contents = None}
- : unit = ()
- : int list = [1]

The change to lst0 mutates lst1, because they are aliases.
lst0 的更改会改变 lst1 ,因为它们是别名。

The type of empty. Returning to empty, why must it be a function? It might seem as though we could define it more simply as follows:
empty 的类型。回到 empty ,为什么它必须是一个函数?看起来我们可以更简单地定义它,如下所示:

let empty = ref None
val empty : '_weak3 option ref = {contents = None}

But now there is only ever one ref that gets created, hence there is only one list ever in existence:
但现在只创建了一个引用,因此只存在一个列表:

let lst2 = empty;;
let lst3 = empty;;
insert_first lst2 2;;
insert_first lst3 3;;
to_list lst2;;
to_list lst3;;
val lst2 : '_weak3 option ref = {contents = None}
val lst3 : '_weak3 option ref = {contents = None}
- : unit = ()
- : unit = ()
- : int list = [3; 2]
- : int list = [3; 2]

Note how the mutations affect both lists, because they are both aliases for the same ref.
请注意突变如何影响两个列表,因为它们都是同一引用的别名。

By correctly making empty a function, we guarantee that a new ref is returned every time an empty list is created.
通过正确地使 empty 成为一个函数,我们保证每次创建空列表时都会返回一个新的引用。

let empty () = ref None
val empty : unit -> 'a option ref = <fun>

It really doesn’t matter what argument that function takes, since it will never use it. We could define it as any of these in principle:
该函数采用什么参数实际上并不重要,因为它永远不会使用它。原则上我们可以将其定义为以下任何一个:

let empty _ = ref None
let empty (b : bool) = ref None
let empty (n : int) = ref None
(* etc. *)
val empty : 'a -> 'b option ref = <fun>
val empty : bool -> 'a option ref = <fun>
val empty : int -> 'a option ref = <fun>

But the reason we prefer unit as the argument type is to indicate to the client that the argument value is not going to be used. After all, there’s nothing interesting that the function can do with the unit value. Another way to think about that would be that a function whose input type is unit is like a function or method in an imperative language that takes in no arguments. For example, in Java a linked list class could have a constructor that takes no arguments and creates an empty list:
但我们更喜欢 unit 作为参数类型的原因是向客户端表明该参数值不会被使用。毕竟,该函数对单位值没有什么有趣的作用。另一种思考方式是,输入类型为 unit 的函数就像命令式语言中不接受任何参数的函数或方法。例如,在 Java 中,链表类可以有一个不带参数并创建空列表的构造函数:

class LinkedList {
  /** Returns an empty list. */
  LinkedList() { ... }
}

Mutable values. In mlist, the nodes of the list are mutable, but the values are not. If we wanted the values also to be mutable, we can make them refs too:
可变的值。在 mlist 中,列表的节点是可变的,但值不是可变的。如果我们希望这些值也是可变的,我们也可以将它们设为引用:

type 'a node = { next : 'a mlist; value : 'a ref }
and 'a mlist = 'a node option ref

let empty () : 'a mlist = ref None

let insert_first (lst : 'a mlist) (v : 'a) : unit =
  lst := Some { next = ref !lst; value = ref v }

let rec set (lst : 'a mlist) (n : int) (v : 'a) : unit =
  match (!lst, n) with
  | None, _ -> invalid_arg "out of bounds"
  | Some { value }, 0 -> value := v
  | Some { next }, _ -> set next (n - 1) v

let rec to_list (lst : 'a mlist) : 'a list =
  match !lst with None -> [] | Some { next; value } -> !value :: to_list next
type 'a node = { next : 'a mlist; value : 'a ref; }
and 'a mlist = 'a node option ref
val empty : unit -> 'a mlist = <fun>
val insert_first : 'a mlist -> 'a -> unit = <fun>
val set : 'a mlist -> int -> 'a -> unit = <fun>
val to_list : 'a mlist -> 'a list = <fun>

Now rather than having to create new nodes if we want to change a value, we can directly mutate the value in a node:
现在,如果我们想要更改值,则不必创建新节点,而是可以直接更改节点中的值:

let lst = empty ();;
insert_first lst 42;;
insert_first lst 41;;
to_list lst;;
set lst 1 43;;
to_list lst;;
val lst : '_weak4 mlist = {contents = None}
- : unit = ()
- : unit = ()
- : int list = [41; 42]
- : unit = ()
- : int list = [41; 43]

7.2. Mutable Fields 7.2. 可变字段 ¶

The fields of a record can be declared as mutable, meaning their contents can be updated without constructing a new record. For example, here is a record type for two-dimensional colored points whose color field c is mutable:
记录的字段可以声明为可变的,这意味着可以更新其内容而无需构造新记录。例如,下面是二维彩色点的记录类型,其颜色字段 c 是可变的:

type point = {x : int; y : int; mutable c : string}
type point = { x : int; y : int; mutable c : string; }

Note that mutable is a property of the field, rather than the type of the field. In particular, we write mutable field : type, not field : mutable type.
请注意, mutable 是字段的属性,而不是字段的类型。特别是,我们写 mutable field : type ,而不是 field : mutable type

The operator to update a mutable field is <- which is meant to look like a left arrow.
更新可变字段的运算符是 <- ,它看起来像向左箭头。

let p = {x = 0; y = 0; c = "red"}
val p : point = {x = 0; y = 0; c = "red"}
p.c <- "white"
- : unit = ()
p
- : point = {x = 0; y = 0; c = "white"}

Non-mutable fields cannot be updated that way:
非可变字段不能以这种方式更新:

p.x <- 3;;
File "[5]", line 1, characters 0-8:
1 | p.x <- 3;;
    ^^^^^^^^
Error: The record field x is not mutable
  • Syntax: e1.f <- e2 语法: e1.f <- e2

  • Dynamic semantics: To evaluate e1.f <- e2, evaluate e2 to a value v2, and e1 to a value v1, which must have a field named f. Update v1.f to v2. Return ().
    动态语义:要计算 e1.f <- e2 ,请将 e2 计算为值 v2 ,将 e1 计算为值 v1 ,它必须有一个名为 f 的字段。将 v1.f 更新为 v2 。返回 ()

  • Static semantics: e1.f <- e2 : unit if e1 : t1 and t1 = {...; mutable f : t2; ...}, and e2 : t2.
    静态语义: e1.f <- e2 : unit 如果 e1 : t1t1 = {...; mutable f : t2; ...} ,以及 e2 : t2

7.2.1. Refs Are Mutable Fields
7.2.1. Refs 是可变字段 ¶

It turns out that refs are actually implemented as mutable fields. In Stdlib we find the following declaration:
事实证明,refs 实际上是作为可变字段实现的。在 Stdlib 中我们找到以下声明:

type 'a ref = { mutable contents : 'a }

And that’s why when the toplevel outputs a ref it looks like a record: it is a record with a single mutable field named contents!
这就是为什么当顶层输出一个 ref 时,它看起来像一条记录:它是一条带有名为 contents 的单个可变字段的记录!

let r = ref 42
val r : int ref = {contents = 42}

The other syntax we’ve seen for refs is in fact equivalent to simple OCaml functions:
我们看到的 refs 的其他语法实际上相当于简单的 OCaml 函数:

let ref x = {contents = x}
val ref : 'a -> 'a ref = <fun>
let ( ! ) r = r.contents
val ( ! ) : 'a ref -> 'a = <fun>
let ( := ) r x = r.contents <- x
val ( := ) : 'a ref -> 'a -> unit = <fun>

The reason we say “equivalent” is that those functions are actually implemented not in OCaml itself but in the OCaml run-time, which is implemented mostly in C. Nonetheless the functions do behave the same as the OCaml source given above.
我们说“等效”的原因是这些函数实际上不是在 OCaml 本身中实现的,而是在 OCaml 运行时中实现的,而 OCaml 运行时主要是在 C 中实现的。尽管如此,这些函数的行为确实与上面给出的 OCaml 源代码相同。

7.2.2. Example: Mutable Singly-Linked Lists
7.2.2. 示例:可变单链表 ¶

Using mutable fields, we can implement singly-linked lists almost the same as we did with references. The types for nodes and lists are simplified:
使用可变字段,我们可以实现单链表,几乎与使用引用一样。节点和列表的类型已简化:

(** An ['a node] is a node of a mutable singly-linked list. It contains a value
    of type ['a] and optionally has a pointer to the next node. *)
type 'a node = {
  mutable next : 'a node option;
  value : 'a
}

(** An ['a mlist] is a mutable singly-linked list with elements of type ['a].
    RI: The list does not contain any cycles. *)
type 'a mlist = {
  mutable first : 'a node option;
}
type 'a node = { mutable next : 'a node option; value : 'a; }
type 'a mlist = { mutable first : 'a node option; }

And there is no essential difference in the algorithms for implementing the operations, but the code is slightly simplified because we don’t have to use reference operations:
实现操作的算法没有本质区别,但代码稍微简化了,因为我们不必使用引用操作:

(** [insert_first lst n] mutates mlist [lst] by inserting value [v] as the
    first value in the list. *)
let insert_first (lst : 'a mlist) (v : 'a) =
  lst.first <- Some {value = v; next = lst.first}

(** [empty ()] is an empty singly-linked list. *)
let empty () : 'a mlist = {
  first = None
}

(** [to_list lst] is an OCaml list containing the same values as [lst]
    in the same order. Not tail recursive. *)
let to_list (lst : 'a mlist) : 'a list =
  let rec helper = function
    | None -> []
    | Some {next; value} -> value :: helper next
  in
  helper lst.first
val insert_first : 'a mlist -> 'a -> unit = <fun>
val empty : unit -> 'a mlist = <fun>
val to_list : 'a mlist -> 'a list = <fun>

7.2.3. Example: Mutable Stacks
7.2.3. 示例:可变堆栈 ¶

We already know that lists and stacks can be implemented in quite similar ways. Let’s use what we’ve learned from mutable linked lists to implement mutable stacks. Here is an interface:
我们已经知道列表和堆栈可以通过非常相似的方式实现。让我们使用从可变链表中学到的知识来实现​​可变堆栈。这是一个界面:

module type MutableStack = sig
  (** ['a t] is the type of mutable stacks whose elements have type ['a].
      The stack is mutable not in the sense that its elements can
      be changed, but in the sense that it is not persistent:
      the operations [push] and [pop] destructively modify the stack. *)
  type 'a t

  (** Raised if [peek] or [pop] encounter the empty stack. *)
  exception Empty

  (** [empty ()] is the empty stack *)
  val empty : unit -> 'a t

  (** [push x s] modifies [s] to make [x] its top element.
      The rest of the elements are unchanged. *)
  val push : 'a -> 'a t -> unit

  (**[peek s] is the top element of [s].
     Raises: [Empty] if [s] is empty. *)
  val peek : 'a t -> 'a

  (** [pop s] removes the top element of [s].
      Raises: [Empty] if [s] is empty. *)
  val pop : 'a t -> unit
end
module type MutableStack =
  sig
    type 'a t
    exception Empty
    val empty : unit -> 'a t
    val push : 'a -> 'a t -> unit
    val peek : 'a t -> 'a
    val pop : 'a t -> unit
  end

Now let’s implement the mutable stack with a mutable linked list.
现在让我们用可变链表来实现可变堆栈。

module MutableRecordStack : MutableStack = struct
  (** An ['a node] is a node of a mutable linked list.  It has
     a field [value] that contains the node's value, and
     a mutable field [next] that is [None] if the node has
     no successor, or [Some n] if the successor is [n]. *)
  type 'a node = {value : 'a; mutable next : 'a node option}

 (** AF: An ['a t] is a stack represented by a mutable linked list.
     The mutable field [top] is the first node of the list,
     which is the top of the stack. The empty stack is represented
     by {top = None}.  The node {top = Some n} represents the
     stack whose top is [n], and whose remaining elements are
     the successors of [n]. *)
  type 'a t = {mutable top : 'a node option}

  exception Empty

  let empty () = {top = None}

  let push x s = s.top <- Some {value = x; next = s.top}

  let peek s =
    match s.top with
    | None -> raise Empty
    | Some {value} -> value

  let pop s =
    match s.top with
    | None -> raise Empty
    | Some {next} -> s.top <- next
end
module MutableRecordStack : MutableStack

7.3. Arrays and Loops
7.3. 数组和循环 ¶

Arrays are fixed-length mutable sequences with constant-time access and update. So they are similar in various ways to refs, lists, and tuples. Like refs, they are mutable. Like lists, they are (finite) sequences. Like tuples, their length is fixed in advance and cannot be resized.
数组是固定长度的可变序列,具有恒定时间的访问和更新。因此它们在很多方面与引用、列表和元组相似。就像裁判一样,它们是可变的。与列表一样,它们是(有限)序列。与元组一样,它们的长度是预先固定的并且无法调整大小。

The syntax for arrays is similar to lists:
数组的语法与列表类似:

let v = [|0.; 1.|]
val v : float array = [|0.; 1.|]

That code creates an array whose length is fixed to be 2 and whose contents are initialized to 0. and 1.. The keyword array is a type constructor, much like list.
该代码创建一个长度固定为 2 的数组,其内容初始化为 0.1. 。关键字 array 是一个类型构造函数,非常类似于 list

Later those contents can be changed using the <- operator:
稍后可以使用 <- 运算符更改这些内容:

v.(0) <- 5.
- : unit = ()
v
- : float array = [|5.; 1.|]

As you can see in that example, indexing into an array uses the syntax array.(index), where the parentheses are mandatory.
正如您在该示例中看到的,对数组进行索引使用语法 array.(index) ,其中括号是必需的。

The Array module has many useful functions on arrays.
Array 模块在数组上有许多有用的函数。

Syntax.

  • Array creation: [|e0; e1; ...; en|]
    数组创建: [|e0; e1; ...; en|]

  • Array indexing: e1.(e2)
    数组索引: e1.(e2)

  • Array assignment: e1.(e2) <- e3
    数组赋值: e1.(e2) <- e3

Dynamic semantics. 动态语义。

  • To evaluate [|e0; e1; ...; en|], evaluate each ei to a value vi, create a new array of length n+1, and store each value in the array at its index.
    要计算 [|e0; e1; ...; en|] ,请将每个 ei 计算为值 vi ,创建一个长度为 n+1 的新数组,并将每个值存储在数组位于其索引处。

  • To evaluate e1.(e2), evaluate e1 to an array value v1, and e2 to an integer v2. If v2 is not within the bounds of the array (i.e., 0 to n-1, where n is the length of the array), raise Invalid_argument. Otherwise, index into v1 to get the value v at index v2, and return v.
    要计算 e1.(e2) ,请将 e1 计算为数组值 v1 ,将 e2 计算为整数 v2 。如果 v2 不在数组范围内(即 0n-1 ,其中 n 是数组的长度) ,提高 Invalid_argument 。否则,索引 v1 以获取索引 v2 处的值 v ,并返回 v

  • To evaluate e1.(e2) <- e3, evaluate each expression ei to a value vi. Check that v2 is within bounds, as in the semantics of indexing. Mutate the element of v1 at index v2 to be v3.
    要计算 e1.(e2) <- e3 ,请将每个表达式 ei 计算为值 vi 。检查 v2 是否在范围内,如索引语义所示。将索引 v2 处的 v1 元素突变为 v3

Static semantics. 静态语义。

  • [|e0; e1; ...; en|] : t array if ei : t for all the ei.
    [|e0; e1; ...; en|] : t array 如果 ei : t 对于所有 ei

  • e1.(e2) : t if e1 : t array and e2 : int.
    e1.(e2) : t 如果 e1 : t arraye2 : int

  • e1.(e2) <- e3 : unit if e1 : t array and e2 : int and e3 : t.
    e1.(e2) <- e3 : unit 如果 e1 : t arraye2 : inte3 : t

Loops.

OCaml has while loops and for loops. Their syntax is as follows:
OCaml 有 while 循环和 for 循环。它们的语法如下:

while e1 do e2 done
for x=e1 to e2 do e3 done
for x=e1 downto e2 do e3 done

Each of these three expressions evaluates the expression between do and done for each iteration of the loop; while loops terminate when e1 becomes false; for loops execute once for each integer from e1 to e2; for..to loops evaluate starting at e1 and incrementing x each iteration; for..downto loops evaluate starting at e1 and decrementing x each iteration. All three expressions evaluate to () after the termination of the loop. Because they always evaluate to (), they are less general than folds, maps, or recursive functions.
对于循环的每次迭代,这三个表达式中的每一个都会计算 dodone 之间的表达式;当 e1 变为 false 时, while 循环终止; for 循环对 e1e2 之间的每个整数执行一次; for..to 循环从 e1 开始计算,每次迭代递增 xfor..downto 循环从 e1 开始计算,每次迭代递减 x 。循环终止后,所有三个表达式的计算结果均为 () 。因为它们总是评估为 () ,所以它们不如折叠、映射或递归函数那么通用。

Loops are themselves not inherently mutable, but they are most often used in conjunction with mutable features like arrays—typically, the body of the loop causes side effects. We can also use functions like Array.iter, Array.map, and Array.fold_left instead of loops.
循环本身并不可变,但它们最常与可变功能(如数组)结合使用 - 通常,循环体会产生副作用。我们还可以使用 Array.iterArray.mapArray.fold_left 等函数来代替循环。

7.4. Summary 7.4. 小结 ¶

Mutable data types make programs harder to reason about. For example, before refs, we didn’t have to worry about aliasing in OCaml. But mutability does have its uses. I/O is fundamentally about mutation. And some data structures (like arrays and hash tables) cannot be implemented as efficiently without mutability.
可变数据类型使程序更难以推理。例如,在 refs 之前,我们不必担心 OCaml 中的别名。但可变性确实有其用途。 I/O 本质上是关于突变的。如果没有可变性,某些数据结构(如数组和哈希表)就无法有效实现。

Mutability thus offers great power, but with great power comes great responsibility. Try not to abuse your new-found power!
因此,可变性提供了巨大的力量,但力量越大,责任也越大。尽量不要滥用你新发现的力量!

7.4.1. Terms and Concepts
7.4.1. 术语和概念 ¶

  • address 地址

  • alias 别名

  • array 数组

  • assignment 任务

  • dereference 解引用

  • deterministic 确定性的

  • immutable 不可变的

  • index 指数

  • loop 环形

  • memory safety 内存安全

  • mutable 可变的

  • mutable field 可变字段

  • nondeterministic 不确定性的

  • physical equality 身体平等

  • pointer 指针

  • pure 纯的

  • ref

  • ref cell 引用单元

  • reference 引用

  • sequencing 测序

  • structural equality 结构平等

7.4.2. Further Reading 7.4.2. 延伸阅读 ¶

  • Introduction to Objective Caml, chapters 7 and 8.
    Objective Caml 简介,第 7 章和第 8 章。

  • OCaml from the Very Beginning, chapter 13.
    OCaml 从头开始​​,第 13 章。

  • Real World OCaml, chapters 8.
    现实世界 OCaml,第 8 章。

7.5. Exercises 7.5. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: mutable fields [★]
练习:可变字段[★]

Define an OCaml record type to represent student names and GPAs. It should be possible to mutate the value of a student’s GPA. Write an expression defining a student with name "Alice" and GPA 3.7. Then write an expression to mutate Alice’s GPA to 4.0.
定义 OCaml 记录类型来表示学生姓名和 GPA。改变学生 GPA 的值应该是可能的。编写一个表达式,定义一个名为 "Alice" 和 GPA 3.7 的学生。然后编写一个表达式将 Alice 的 GPA 突变为 4.0


Exercise: refs [★] 练习:引用[★]

Give OCaml expressions that have the following types. Use utop to check your answers.
给出具有以下类型的 OCaml 表达式。使用 utop 检查您的答案。

  • bool ref

  • int list ref

  • int ref list


Exercise: inc fun [★] 练习:增值功能[★]

Define a reference to a function as follows:
定义对函数的引用如下:

let inc = ref (fun x -> x + 1)

Write code that uses inc to produce the value 3110.
编写使用 inc 生成值 3110 的代码。


Exercise: addition assignment [★★]
练习:加法作业[★★]

The C language and many languages derived from it, such as Java, has an addition assignment operator written a += b and meaning a = a + b. Implement such an operator in OCaml; its type should be int ref -> int -> unit. Here’s some code to get you started:
C 语言和许多由它派生的语言(例如 Java)都有一个加法赋值运算符,写作 a += b ,含义为 a = a + b 。在 OCaml 中实现这样一个运算符;它的类型应该是 int ref -> int -> unit 。下面是一些可以帮助您入门的代码:

let ( +:= ) x y = ...

And here’s an example usage:
这是一个用法示例:

# let x = ref 0;;
# x +:= 3110;;
# !x;;
- : int = 3110

Exercise: physical equality [★★]
运动:身体平等[★★]

Define x, y, and z as follows:
定义 xyz 如下:

let x = ref 0
let y = x
let z = ref 0

Predict the value of the following series of expressions:
预测以下一系列表达式的值:

# x == y;;
# x == z;;
# x = y;;
# x = z;;
# x := 1;;
# x = y;;
# x = z;;

Check your answers in utop.
在 utop 中检查您的答案。


Exercise: norm [★★] 练习:正常[★★]

The Euclidean norm of an n-dimensional vector x=(x1,,xn) is written |x| and is defined to be
n 维向量 x=(x1,,xn) 的欧几里得范数写作 |x| 并定义为

x12++xn2.

Write a function norm : vector -> float that computes the Euclidean norm of a vector, where vector is defined as follows:
编写一个函数 norm : vector -> float 来计算向量的欧几里德范数,其中 vector 定义如下:

(* AF: the float array [| x1; ...; xn |] represents the
 *     vector (x1, ..., xn)
 * RI: the array is non-empty *)
type vector = float array

Your function should not mutate the input array. Hint: although your first instinct might be to reach for a loop, instead try to use Array.map and Array.fold_left or Array.fold_right.
您的函数不应改变输入数组。提示:尽管您的第一反应可能是进行循环,但请尝试使用 Array.mapArray.fold_leftArray.fold_right


Exercise: normalize [★★] 锻炼:正常化[★★]

Every vector can be normalized by dividing each component by |x|; this yields a vector with norm 1:
每个向量都可以通过将每个分量除以 |x| 来标准化;这会产生一个范数为 1 的向量:

(x1|x|,,xn|x|)

Write a function normalize : vector -> unit that normalizes a vector “in place” by mutating the input array. Here’s a sample usage:
编写一个函数 normalize : vector -> unit ,通过改变输入数组来“就地”标准化向量。这是一个示例用法:

# let a = [|1.; 1.|];;
val a : float array = [|1.; 1.|]

# normalize a;;
- : unit = ()

# a;;
- : float array = [|0.7071...; 0.7071...|]

Hint: Array.iteri. 提示: Array.iteri


Exercise: norm loop [★★] 练习:范数循环[★★]

Modify your implementation of norm to use a loop. Here is pseudocode for what you should do:
修改 norm 的实现以使用循环。这是您应该做什么的伪代码:

initialize norm to 0.0
loop through array
  add to norm the square of the current array component
return sqrt of norm

Exercise: normalize loop [★★]
练习:标准化循环[★★]

Modify your implementation of normalize to use a loop.
修改 normalize 的实现以使用循环。


Exercise: init matrix [★★★]
练习:初始化矩阵[★★★]

The Array module contains two functions for creating an array: make and init. make creates an array and fills it with a default value, while init creates an array and uses a provided function to fill it in. The library also contains a function make_matrix for creating a two-dimensional array, but it does not contain an analogous init_matrix to create a matrix using a function for initialization.
Array 模块包含两个用于创建数组的函数: makeinitmake 创建一个数组并用默认值填充它,而 init 创建一个数组并使用提供的函数来填充它。该库还包含一个函数 make_matrix 使用初始化函数创建矩阵。

Write a function init_matrix : int -> int -> (int -> int -> 'a) -> 'a array array such that init_matrix n o f creates and returns an n by o matrix m with m.(i).(j) = f i j for all i and j in bounds.
编写一个函数 init_matrix : int -> int -> (int -> int -> 'a) -> 'a array array ,以便 init_matrix n o f 创建并返回 n by o 矩阵 mm.(i).(j) = f i j 对于边界内的所有 ij

See the documentation for make_matrix for more information on the representation of matrices as arrays.
有关将矩阵表示为数组的更多信息,请参阅 make_matrix 的文档。

8. Data Structures 8. 数据结构 ¶

Efficient data structures are important building blocks for large programs. In this chapter, we’ll discuss what it means to be efficient, how to implement some efficient data structures using both imperative and functional programming, and learn about the technique of amortized analysis.
高效的数据结构是大型程序的重要构建块。在本章中,我们将讨论高效意味着什么,如何使用命令式编程和函数式编程来实现一些高效的数据结构,并了解摊销分析技术。

Of course, we’ve already covered quite a few simple data structures, especially in the modules chapter, where we used lists to implement stacks, queues, maps, and sets. For stacks and (batched) queues, those implementations were already efficient. But we can do much better for maps (and sets). In this chapter we’ll see efficient implementations of maps using hash tables and red-black trees.
当然,我们已经介绍了相当多的简单数据结构,特别是在模块章节中,我们使用列表来实现堆栈、队列、映射和集合。对于堆栈和(批处理)队列,这些实现已经非常高效。但我们可以在地图(和集合)方面做得更好。在本章中,我们将看到使用哈希表和红黑树的映射的高效实现。

We’ll also take a look at some cool functional data structures that appear less often in imperative languages: sequences, which are infinite lists implemented with functions called thunks; lazy lists, which are implemented with a language feature (aptly called “laziness”) that suspends evaluation; promises, which are a way of organizing concurrent computations that has recently become popular in imperative web programming; and monads, which are a way of organizing any kind of computation that has (side) effects.
我们还将看看一些很酷的函数式数据结构,它们在命令式语言中很少出现:序列,它是用称为 thunk 的函数实现的无限列表;惰性列表,它是通过暂停评估的语言功能(恰当地称为“惰性”)实现的; Promise,这是一种组织并发计算的方式,最近在命令式 Web 编程中变得流行;和 monad,它们是组织任何具有(副作用)影响的计算的一种方式。

8.1. Hash Tables 8.1. 哈希表 ¶

The hash table is a widely used data structure whose performance relies upon mutability. The implementation of a hash table is quite involved compared to other data structures we’ve implemented so far. We’ll build it up slowly, so that the need for and use of each piece can be appreciated.
哈希表是一种广泛使用的数据结构,其性能依赖于可变性。与我们迄今为止实现的其他数据结构相比,哈希表的实现相当复杂。我们将慢慢地构建它,以便能够理解每件作品的需求和使用。

8.1.1. Maps 8.1.1. 映射 ¶

Hash tables implement the map data abstraction. A map binds keys to values. This abstraction is so useful that it goes by many other names, among them associative array, dictionary, and symbol table. We’ll write maps abstractly (i.e, mathematically; not actually OCaml syntax) as { k1:v1,k2:v2,,kn:vn }. Each k:v is a binding of key k to value v. Here are a couple of examples:
哈希表实现了地图数据抽象。映射将键绑定到值。这种抽象非常有用,因此有许多其他名称,其中包括关联数组、字典和符号表。我们将抽象地(即,数学上的;实际上不是 OCaml 语法)编写映射为 { k1:v1,k2:v2,,kn:vn }。每个 k:v 都是键 k 到值 v 的绑定。这里有几个例子:

  • A map binding a course number to something about it: {3110 : “Fun”, 2110 : “OO”}.
    将课程编号与其相关内容绑定的地图:{3110:“Fun”,2110:“OO”}。

  • A map binding a university name to the year it was chartered: {“Harvard” : 1636, “Princeton” : 1746, “Penn”: 1740, “Cornell” : 1865}.
    将大学名称与其成立年份绑定在一起的地图:{“哈佛”:1636 年,“普林斯顿”:1746 年,“宾夕法尼亚州”:1740 年,“康奈尔”:1865 年}。

The order in which the bindings are abstractly written does not matter, so the first example might also be written {2110 : “OO”, 3110 : “Fun”}. That’s why we use set braces—they suggest that the bindings are a set, with no ordering implied.
抽象地编写绑定的顺序并不重要,因此第一个示例也可以编写为 {2110 : “OO”, 3110 : “Fun”}。这就是我们使用大括号的原因——它们表明绑定是一个集合,没有隐含的顺序。

Note 笔记

As that notation suggests, maps and sets are very similar. Data structures that can implement a set can also implement a map, and vice-versa:
正如该符号所示,地图和集合非常相似。可以实现集合的数据结构也可以实现映射,反之亦然:

  • Given a map data structure, we can treat the keys as elements of a set, and simply ignore the values which the keys are bound to. This admittedly wastes a little space, because we never need the values.
    给定一个映射数据结构,我们可以将键视为集合的元素,并简单地忽略键所绑定的值。这无疑浪费了一点空间,因为我们从来不需要这些值。

  • Given a set data structure, we can store key–value pairs as the elements. Searching for elements (hence insertion and removal) might become more expensive, because the set abstraction is unlikely to support searching for keys by themselves.
    给定一个集合数据结构,我们可以将键值对存储为元素。搜索元素(因此插入和删除)可能会变得更加昂贵,因为集合抽象不太可能支持自行搜索键。

Here is an interface for maps:
这是地图的界面:

module type Map = sig

  (** [('k, 'v) t] is the type of maps that bind keys of type
      ['k] to values of type ['v]. *)
  type ('k, 'v) t

  (** [insert k v m] is the same map as [m], but with an additional
      binding from [k] to [v].  If [k] was already bound in [m],
      that binding is replaced by the binding to [v] in the new map. *)
  val insert : 'k -> 'v -> ('k, 'v) t -> ('k, 'v) t

  (** [find k m] is [Some v] if [k] is bound to [v] in [m],
      and [None] if not. *)
  val find : 'k -> ('k, 'v) t -> 'v option

  (** [remove k m] is the same map as [m], but without any binding of [k].
      If [k] was not bound in [m], then the map is unchanged. *)
  val remove : 'k -> ('k, 'v) t -> ('k, 'v) t

  (** [empty] is the empty map. *)
  val empty : ('k, 'v) t

  (** [of_list lst] is a map containing the same bindings as
      association list [lst].
      Requires: [lst] does not contain any duplicate keys. *)
  val of_list : ('k * 'v) list -> ('k, 'v) t

  (** [bindings m] is an association list containing the same
      bindings as [m]. There are no duplicates in the list. *)
  val bindings : ('k, 'v) t -> ('k * 'v) list
end
module type Map =
  sig
    type ('k, 'v) t
    val insert : 'k -> 'v -> ('k, 'v) t -> ('k, 'v) t
    val find : 'k -> ('k, 'v) t -> 'v option
    val remove : 'k -> ('k, 'v) t -> ('k, 'v) t
    val empty : ('k, 'v) t
    val of_list : ('k * 'v) list -> ('k, 'v) t
    val bindings : ('k, 'v) t -> ('k * 'v) list
  end

Next, we’re going to examine three implementations of maps based on
接下来,我们将基于以下内容检查地图的三种实现

  • association lists, 协会名单,

  • arrays, and 数组,以及

  • a combination of the above known as a hash table with chaining.
    上述的组合称为带有链接的哈希表。

Each implementation will need a slightly different interface, because of constraints resulting from the underlying representation type. In each case we’ll pay close attention to the AF, RI, and efficiency of the operations.
由于底层表示类型的限制,每个实现都需要稍微不同的接口。在每种情况下,我们都会密切关注 AF、RI 和操作效率。

8.1.2. Maps as Association Lists
8.1.2. Maps 作为键值列表 ¶

The simplest implementation of a map in OCaml is as an association list. We’ve seen that representation twice so far [1] [2]. Here is an implementation of Map using it:
OCaml 中映射的最简单实现是作为关联列表。到目前为止,我们已经见过这种表示两次了 [1][2]。这是使用它的 Map 的实现:

module ListMap : Map = struct
  (** AF: [[(k1, v1); (k2, v2); ...; (kn, vn)]] is the map {k1 : v1, k2 : v2,
      ..., kn : vn}. If a key appears more than once in the list, then in the
      map it is bound to the left-most occurrence in the list. For example,
      [[(k, v1); (k, v2)]] represents {k : v1}. The empty list represents
      the empty map.
      RI: none. *)
  type ('k, 'v) t = ('k * 'v) list

  (** Efficiency: O(1). *)
  let insert k v m = (k, v) :: m

  (** Efficiency: O(n). *)
  let find = List.assoc_opt

  (** Efficiency: O(n). *)
  let remove k lst = List.filter (fun (k', _) -> k <> k') lst

  (** Efficiency: O(1). *)
  let empty = []

  (** Efficiency: O(1). *)
  let of_list lst = lst

  (** [keys m] is a list of the keys in [m], without
      any duplicates.
      Efficiency: O(n log n). *)
  let keys m = m |> List.map fst |> List.sort_uniq Stdlib.compare

  (** [binding m k] is [(k, v)], where [v] is the value that [k]
       binds in [m].
       Requires: [k] is a key in [m].
       Efficiency: O(n). *)
  let binding m k = (k, List.assoc k m)

  (** Efficiency: O(n log n) + O(n) * O(n), which is O(n^2). *)
  let bindings m = List.map (binding m) (keys m)
end
module ListMap : Map

8.1.3. Maps as Arrays
8.1.3. 映射作为数组 ¶

Mutable maps are maps whose bindings may be mutated. The interface for a mutable map therefore differs from a immutable map. Insertion and removal operations for a mutable map therefore return unit, because they do not produce a new map but instead mutate an existing map.
可变映射是其绑定可能发生变化的映射。因此,可变映射的接口与不可变映射的接口不同。因此,可变映射的插入和删除操作会返回 unit ,因为它们不会生成新映射,而是会改变现有映射。

An array can be used to represent a mutable map whose keys are integers. A binding from a key to a value is stored by using the key as an index into the array, and storing the binding at that index. For example, we could use an array to map office numbers to their occupants:
数组可用于表示键为整数的可变映射。通过使用键作为数组的索引,并将绑定存储在该索引处,可以存储从键到值的绑定。例如,我们可以使用一个数组将办公室号码映射到其占用者:

Office 办公室

Occupant 居住者

459

Fan

460

Gries 格里斯

461

Clarkson 克拉克森

462

Muhlberger 米尔伯格

463

does not exist 不存在

This kind of map is called a direct address table. Since arrays have a fixed size, the implementer now needs to know the client’s desire for the capacity of the table (i.e., the number of bindings that can be stored in it) whenever an empty table is created. That leads to the following interface:
这种映射称为直接地址表。由于数组具有固定大小,因此每当创建空表时,实现者现在需要知道客户端对表容量的需求(即可以存储在其中的绑定数量)。这会导致以下界面:

module type DirectAddressMap = sig
  (** [t] is the type of maps that bind keys of type int to values of
      type ['v]. *)
  type 'v t

  (** [insert k v m] mutates map [m] to bind [k] to [v]. If [k] was
      already bound in [m], that binding is replaced by the binding to
      [v] in the new map. Requires: [k] is in bounds for [m]. *)
  val insert : int -> 'v -> 'v t -> unit

  (** [find k m] is [Some v] if [k] is bound to [v] in [m], and [None]
      if not. Requires: [k] is in bounds for [m]. *)
  val find : int -> 'v t -> 'v option

  (** [remove k m] mutates [m] to remove any binding of [k]. If [k] was
      not bound in [m], then the map is unchanged. Requires: [k] is in
      bounds for [m]. *)
  val remove : int -> 'v t -> unit

  (** [create c] creates a map with capacity [c]. Keys [0] through [c-1]
      are _in bounds_ for the map. *)
  val create : int -> 'v t

  (** [of_list c lst] is a map containing the same bindings as
      association list [lst] and with capacity [c]. Requires: [lst] does
      not contain any duplicate keys, and every key in [lst] is in
      bounds for capacity [c]. *)
  val of_list : int -> (int * 'v) list -> 'v t

  (** [bindings m] is an association list containing the same bindings
      as [m]. There are no duplicate keys in the list. *)
  val bindings : 'v t -> (int * 'v) list
end
module type DirectAddressMap =
  sig
    type 'v t
    val insert : int -> 'v -> 'v t -> unit
    val find : int -> 'v t -> 'v option
    val remove : int -> 'v t -> unit
    val create : int -> 'v t
    val of_list : int -> (int * 'v) list -> 'v t
    val bindings : 'v t -> (int * 'v) list
  end

Here is an implementation of that interface:
这是该接口的实现:

module ArrayMap : DirectAddressMap = struct
  (** AF: [[|Some v0; Some v1; ... |]] represents {0 : v0, 1 : v1, ...}.
      If element [i] of the array is instead [None], then [i] is not
      bound in the map.
      RI: None. *)
  type 'v t = 'v option array

  (** Efficiency: O(1) *)
  let insert k v a = a.(k) <- Some v

  (** Efficiency: O(1) *)
  let find k a = a.(k)

  (** Efficiency: O(1) *)
  let remove k a = a.(k) <- None

  (** Efficiency: O(c) *)
  let create c = Array.make c None

  (** Efficiency: O(c) *)
  let of_list c lst =
    (* O(c) *)
    let a = create c in
    (* O(c) * O(1) = O(c) *)
    List.iter (fun (k, v) -> insert k v a) lst;
    a

  (** Efficiency: O(c) *)
  let bindings a =
    let bs = ref [] in
    (* O(1) *)
    let add_binding k v =
      match v with None -> () | Some v -> bs := (k, v) :: !bs
    in
    (* O(c) *)
    Array.iteri add_binding a;
    !bs
end
module ArrayMap : DirectAddressMap

Its efficiency is great! The insert, find, and remove operations are constant time. But that comes at the expense of forcing keys to be integers. Moreover, they need to be small integers (or at least integers from a small range), otherwise the arrays we use will need to be huge.
它的效率是非常高的! insertfindremove 操作是恒定时间。但这是以强制键为整数为代价的。此外,它们必须是小整数(或者至少是小范围的整数),否则我们使用的数组将需要很大。

8.1.4. Maps as Hash Tables
8.1.4. 映射作为哈希表 ¶

Arrays offer constant time performance, but come with severe restrictions on keys. Association lists don’t place those restrictions on keys, but they also don’t offer constant time performance. Is there a way to get the best of both worlds? Yes (more or less)! Hash tables are the solution.
数组提供恒定的时间性能,但对键有严格的限制。关联列表不会对键施加这些限制,但它们也不提供恒定的时间性能。有没有一种方法可以两全其美?是的(或多或少)!哈希表就是解决方案。

The key idea is that we assume the existence of a hash function hash : 'a -> int that can convert any key to a non-negative integer. Then we can use that function to index into an array, as we did with direct address tables. Of course, we want the hash function itself to run in constant time, otherwise the operations that use it would not be efficient.
关键思想是我们假设存在一个哈希函数 hash : 'a -> int ,它可以将任何键转换为非负整数。然后我们可以使用该函数对数组进行索引,就像我们对直接地址表所做的那样。当然,我们希望哈希函数本身能够在恒定时间内运行,否则使用它的操作将不会高效。

That leads to the following interface, in which the client of the hash table has to pass in a hash function when a table is created:
这就导致了以下接口,其中哈希表的客户端在创建表时必须传入哈希函数:

module type TableMap = sig
  (** [('k, 'v) t] is the type of mutable table-based maps that bind
      keys of type ['k] to values of type ['v]. *)
  type ('k, 'v) t

  (** [insert k v m] mutates map [m] to bind [k] to [v]. If [k] was
      already bound in [m], that binding is replaced by the binding to
      [v]. *)
  val insert : 'k -> 'v -> ('k, 'v) t -> unit

  (** [find k m] is [Some v] if [m] binds [k] to [v], and [None] if [m]
      does not bind [k]. *)
  val find : 'k -> ('k, 'v) t -> 'v option

  (** [remove k m] mutates [m] to remove any binding of [k]. If [k] was
      not bound in [m], the map is unchanged. *)
  val remove : 'k -> ('k, 'v) t -> unit

  (** [create hash c] creates a new table map with capacity [c] that
      will use [hash] as the function to convert keys to integers.
      Requires: The output of [hash] is always non-negative, and [hash]
      runs in constant time. *)
  val create : ('k -> int) -> int -> ('k, 'v) t

  (** [bindings m] is an association list containing the same bindings
      as [m]. *)
  val bindings : ('k, 'v) t -> ('k * 'v) list

  (** [of_list hash lst] creates a map with the same bindings as [lst],
      using [hash] as the hash function. Requires: [lst] does not
      contain any duplicate keys. *)
  val of_list : ('k -> int) -> ('k * 'v) list -> ('k, 'v) t
end
module type TableMap =
  sig
    type ('k, 'v) t
    val insert : 'k -> 'v -> ('k, 'v) t -> unit
    val find : 'k -> ('k, 'v) t -> 'v option
    val remove : 'k -> ('k, 'v) t -> unit
    val create : ('k -> int) -> int -> ('k, 'v) t
    val bindings : ('k, 'v) t -> ('k * 'v) list
    val of_list : ('k -> int) -> ('k * 'v) list -> ('k, 'v) t
  end

One immediate problem with this idea is what to do if the output of the hash is not within the bounds of the array. It’s easy to solve this: if a is the length of the array then computing (hash k) mod a will return an index that is within bounds.
这一想法的一个直接问题是,如果哈希的输出不在数组的范围内,该怎么办。解决这个问题很容易:如果 a 是数组的长度,那么计算 (hash k) mod a 将返回一个在边界内的索引。

Another problem is what to do if the hash function is not injective, meaning that it is not one-to-one. Then multiple keys could collide and need to be stored at the same index in the array. That’s okay! We deliberately allow that. But it does mean we need a strategy for what to do when keys collide.
另一个问题是,如果哈希函数不是单射的,即不是一对一的,该怎么办。那么多个键可能会发生冲突,并且需要存储在数组中的同一索引处。没关系!我们故意允许这种情况发生。但这确实意味着我们需要一个策略来应对按键冲突时的操作。

There are two well-known strategies for dealing with collisions. One is to store multiple bindings at each array index. The array elements are called buckets. Typically, the bucket is implemented as a linked list. This strategy is known by many names, including chaining, closed addressing, and open hashing. We’ll use chaining as the name. To check whether an element is in the hash table, the key is first hashed to find the correct bucket to look in. Then, the linked list is scanned to see if the desired element is present. If the linked list is short, this scan is very quick. An element is added or removed by hashing it to find the correct bucket. Then, the bucket is checked to see if the element is there, and finally the element is added or removed appropriately from the bucket in the usual way for linked lists.
有两种众所周知的处理碰撞的策略。一种是在每个数组索引处存储多个绑定。数组元素称为桶。通常,存储桶被实现为链表。这种策略有很多名称,包括链接、封闭寻址和开放散列。我们将使用链接作为名称。要检查哈希表中是否存在某个元素,首先对键进行哈希处理以找到要查找的正确存储桶。然后,扫描链表以查看是否存在所需的元素。如果链表很短,则此扫描速度非常快。通过散列元素来添加或删除元素以找到正确的存储桶。然后,检查存储桶以查看该元素是否存在,最后以链表的常用方式将元素适当地添加到存储桶中或从存储桶中删除。

The other strategy is to store bindings at places other than their proper location according to the hash. When adding a new binding to the hash table would create a collision, the insert operation instead finds an empty location in the array to put the binding. This strategy is (confusingly) known as probing, open addressing, and closed hashing. We’ll use probing as the name. A simple way to find an empty location is to search ahead through the array indices with a fixed stride (often 1), looking for an unused entry; this linear probing strategy tends to produce a lot of clustering of elements in the table, leading to bad performance. A better strategy is to use a second hash function to compute the probing interval; this strategy is called double hashing. Regardless of how probing is implemented, however, the time required to search for or add an element grows rapidly as the hash table fills up.
另一种策略是根据哈希将绑定存储在正确位置以外的位置。当向哈希表添加新绑定会产生冲突时,插入操作会在数组中查找一个空位置来放置绑定。这种策略(令人困惑地)被称为探测、开放寻址和封闭散列。我们将使用探测作为名称。查找空位置的一个简单方法是以固定步幅(通常为 1)向前搜索数组索引,寻找未使用的条目;这种线性探测策略往往会在表中产生大量元素聚集,从而导致性能不佳。更好的策略是使用第二个哈希函数来计算探测间隔;这种策略称为双重散列。然而,无论如何实现探测,搜索或添加元素所需的时间都会随着哈希表填满而迅速增长。

Chaining has often been preferred over probing in software implementations, because it’s easy to implement the linked lists in software. Hardware implementations have often used probing, when the size of the table is fixed by circuitry. But some modern software implementations are re-examining the performance benefits of probing.
在软件实现中,链接通常比探测更受青睐,因为在软件中实现链表很容易。当表的大小由电路固定时,硬件实现经常使用探测。但一些现代软件实现正在重新审视探测的性能优势。

8.1.4.1. Chaining Representation
8.1.4.1. 链接表征 ¶

Here is a representation type for a hash table that uses chaining:
以下是使用链接的哈希表的表征类型:

type ('k, 'v) t = {
  hash : 'k -> int;
  mutable size : int;
  mutable buckets : ('k * 'v) list array
}

The buckets array has elements that are association lists, which store the bindings. The hash function is used to determine which bucket a key goes into. The size is used to keep track of the number of bindings currently in the table, since that would be expensive to compute by iterating over buckets.
buckets 数组包含关联列表元素,用于存储绑定。 hash 函数用于确定密钥进入哪个存储桶。 size 用于跟踪表中当前的绑定数量,因为通过迭代 buckets 进行计算的成本很高。

Here are the AF and RI:
以下是 AF 和 RI:

  (** AF:  If [buckets] is
        [| [(k11,v11); (k12,v12); ...];
           [(k21,v21); (k22,v22); ...];
           ... |]
      that represents the map
        {k11:v11, k12:v12, ...,
         k21:v21, k22:v22, ...,  ...}.
      RI: No key appears more than once in array (so, no
        duplicate keys in association lists).  All keys are
        in the right buckets: if [k] is in [buckets] at index
        [b] then [hash(k) = b]. The output of [hash] must always
        be non-negative. [hash] must run in constant time.*)

What would the efficiency of insert, find, and remove be for this rep type? All require
对于此代表类型, insertfindremove 的效率是多少?全部需要

  • hashing the key (constant time),
    散列密钥(恒定时间),

  • indexing into the appropriate bucket (constant time), and
    索引到适当的存储桶(恒定时间),以及

  • finding out whether the key is already in the association list (linear in the number of elements in that list).
    找出键是否已经在关联列表中(与该列表中元素的数量成线性关系)。

So the efficiency of the hash table depends on the number of elements in each bucket. That, in turn, is determined by how well the hash function distributes keys across all the buckets.
所以哈希表的效率取决于每个桶中元素的数量。反过来,这取决于哈希函数在所有存储桶中分配密钥的程度。

A terrible hash function, such as the constant function fun k -> 42, would put all keys into same bucket. Then every operation would be linear in the number n of bindings in the map—that is, O(n). We definitely don’t want that.
一个糟糕的哈希函数,例如常量函数 fun k -> 42 ,会将所有键放入同一个桶中。那么每个操作都将与映射中的绑定数量 n 成线性关系,即 O(n) 。我们绝对不希望这样。

Instead, we want hash functions that distribute keys more or less randomly across the buckets. Then the expected length of every bucket will be about the same. If we could arrange that, on average, the bucket length were a constant L, then insert, find, and remove would all in expectation run in time O(L).
相反,我们希望哈希函数能够或多或少地在存储桶中随机分配密钥。那么每个桶的预期长度将大致相同。如果我们可以安排,平均来说,桶长度是一个常数 L ,那么 insertfindremove 都将预期按时运行 O(L)

8.1.4.2. Resizing 8.1.4.2. 调整大小 ¶

How could we arrange buckets to have expected constant length? To answer that, let’s think about the number of bindings and buckets in the table. Define the load factor of the table to be
我们怎样才能将桶安排为具有预期的恒定长度?为了回答这个问题,让我们考虑一下表中绑定和存储桶的数量。定义表的负载因子为

number of bindingsnumber of buckets

So a table with 20 bindings and 10 buckets has a load factor of 2, and a table with 10 bindings and 20 buckets has a load factor of 0.5. The load factor is therefore the average number of bindings in a bucket. So if we could keep the load factor constant, we could keep L constant, thereby keeping the performance to (expected) constant time.
因此,具有 20 个绑定和 10 个存储桶的表的负载因子为 2,具有 10 个绑定和 20 个存储桶的表的负载因子为 0.5。因此,负载因子是桶中绑定的平均数量。因此,如果我们可以保持负载因子恒定,我们就可以保持 L 恒定,从而将性能保持在(预期)恒定时间。

Toward that end, note that the number of bindings is not under the control of the hash table implementer—but the number of buckets is. So by changing the number of buckets, the implementer can change the load factor. A common strategy is to keep the load factor from approximately 1/2 to 2. Then each bucket contains only a couple bindings, and expected constant-time performance is guaranteed.
为此,请注意,绑定的数量不受哈希表实现者的控制,但桶的数量却受到控制。因此,通过改变桶的数量,实施者可以改变负载因子。常见的策略是将负载因子保持在大约 1/2 到 2 之间。这样每个存储桶仅包含几个绑定,并且可以保证预期的恒定时间性能。

There’s no way for the implementer to know in advance, though, exactly how many buckets will be needed. So instead, the implementer will have to resize the bucket array whenever the load factor gets too high. Typically the newly allocated bucket will be of a size to restore the load factor to about 1.
然而,实施者无法提前知道到底需要多少个桶。因此,只要负载因子变得太高,实现者就必须调整存储桶数组的大小。通常,新分配的存储桶的大小将使负载因子恢复到大约 1。

Putting those two ideas together, if the load factor reaches 2, then there are twice as many bindings as buckets in the table. So by doubling the size of the array, we can restore the load factor to 1. Similarly, if the load factor reaches 1/2, then there are twice as many buckets as bindings, and halving the size of the array will restore the load factor to 1.
将这两个想法放在一起,如果负载因子达到 2,则表中的绑定数量是桶的两倍。因此,通过将数组大小加倍,我们可以将负载因子恢复为1。同样,如果负载因子达到1/2,则桶的数量是绑定的两倍,将数组大小减半将恢复负载因数为 1。

Resizing the bucket array to become larger is an essential technique for hash tables. Resizing it to become smaller, though, is not essential. As long as the load factor is bounded by a constant from above, we can achieve expected constant bucket length. So not all implementations will reduce the size of the array. Although doing so would recover some space, it might not be worth the effort. That’s especially true if the size of the hash table cycles over time: although sometimes it becomes smaller, eventually it becomes bigger again.
调整桶数组的大小以使其变得更大是哈希表的一项基本技术。不过,调整其大小以使其变小并不是必需的。只要负载系数受上面的常数限制,我们就可以实现预期的恒定铲斗长度。所以并不是所有的实现都会减少数组的大小。虽然这样做可以恢复一些空间,但可能不值得付出努力。如果哈希表的大小随时间循环,则尤其如此:虽然有时它会变小,但最终它会再次变大。

Unfortunately, resizing would seem to ruin our expected constant-time performance though. Insertion of a binding might cause the load factor to go over 2, thus causing a resize. When the resize occurs, all the existing bindings must be rehashed and added to the new bucket array. Thus, insertion has become a worst-case linear time operation! The same is true for removal, if we resize the array to become smaller when the load factor is too low.
不幸的是,调整大小似乎会破坏我们预期的恒定时间性能。插入绑定可能会导致负载因子超过 2,从而导致调整大小。发生调整大小时,必须重新散列所有现有绑定并将其添加到新的存储桶数组中。因此,插入变成了最坏情况的线性时间操作!如果我们在负载因子太低时调整数组大小以使其变小,则删除也是如此。

8.1.4.3. Implementation
8.1.4.3. 实现 ¶

The implementation of a hash table, below, puts together all the pieces we discussed above.
下面的哈希表的实现将我们上面讨论的所有部分组合在一起。

module HashMap : TableMap = struct

  (** AF and RI: above *)
  type ('k, 'v) t = {
    hash : 'k -> int;
    mutable size : int;
    mutable buckets : ('k * 'v) list array
  }

  (** [capacity tab] is the number of buckets in [tab].
      Efficiency: O(1) *)
  let capacity {buckets} =
    Array.length buckets

  (** [load_factor tab] is the load factor of [tab], i.e., the number of
      bindings divided by the number of buckets. *)
  let load_factor tab =
    float_of_int tab.size /. float_of_int (capacity tab)

  (** Efficiency: O(n) *)
  let create hash n =
    {hash; size = 0; buckets = Array.make n []}

  (** [index k tab] is the index at which key [k] should be stored in the
      buckets of [tab].
      Efficiency: O(1) *)
  let index k tab =
    (tab.hash k) mod (capacity tab)

  (** [insert_no_resize k v tab] inserts a binding from [k] to [v] in [tab]
      and does not resize the table, regardless of what happens to the
      load factor.
      Efficiency: expected O(L) *)
  let insert_no_resize k v tab =
    let b = index k tab in (* O(1) *)
    let old_bucket = tab.buckets.(b) in
    tab.buckets.(b) <- (k,v) :: List.remove_assoc k old_bucket; (* O(L) *)
    if not (List.mem_assoc k old_bucket) then
      tab.size <- tab.size + 1;
    ()

  (** [rehash tab new_capacity] replaces the buckets array of [tab] with a new
      array of size [new_capacity], and re-inserts all the bindings of [tab]
      into the new array.  The keys are re-hashed, so the bindings will
      likely land in different buckets.
      Efficiency: O(n), where n is the number of bindings. *)
  let rehash tab new_capacity =
    (* insert [(k, v)] into [tab] *)
    let rehash_binding (k, v) =
      insert_no_resize k v tab
    in
    (* insert all bindings of bucket into [tab] *)
    let rehash_bucket bucket =
      List.iter rehash_binding bucket
    in
    let old_buckets = tab.buckets in
    tab.buckets <- Array.make new_capacity []; (* O(n) *)
    tab.size <- 0;
    (* [rehash_binding] is called by [rehash_bucket] once for every binding *)
    Array.iter rehash_bucket old_buckets (* expected O(n) *)

  (* [resize_if_needed tab] resizes and rehashes [tab] if the load factor
     is too big or too small.  Load factors are allowed to range from
     1/2 to 2. *)
  let resize_if_needed tab =
    let lf = load_factor tab in
    if lf > 2.0 then
      rehash tab (capacity tab * 2)
    else if lf < 0.5 then
      rehash tab (capacity tab / 2)
    else ()

  (** Efficiency: O(n) *)
  let insert k v tab =
    insert_no_resize k v tab; (* O(L) *)
    resize_if_needed tab (* O(n) *)

  (** Efficiency: expected O(L) *)
  let find k tab =
    List.assoc_opt k tab.buckets.(index k tab)

  (** [remove_no_resize k tab] removes [k] from [tab] and does not trigger
      a resize, regardless of what happens to the load factor.
      Efficiency: expected O(L) *)
  let remove_no_resize k tab =
    let b = index k tab in
    let old_bucket = tab.buckets.(b) in
    tab.buckets.(b) <- List.remove_assoc k tab.buckets.(b);
    if List.mem_assoc k old_bucket then
      tab.size <- tab.size - 1;
    ()

  (** Efficiency: O(n) *)
  let remove k tab =
    remove_no_resize k tab; (* O(L) *)
    resize_if_needed tab (* O(n) *)

  (** Efficiency: O(n) *)
  let bindings tab =
    Array.fold_left
      (fun acc bucket ->
         List.fold_left
           (* 1 cons for every binding, which is O(n) *)
           (fun acc (k,v) -> (k,v) :: acc)
           acc bucket)
      [] tab.buckets

  (** Efficiency: O(n^2) *)
  let of_list hash lst =
    let m = create hash (List.length lst) in  (* O(n) *)
    List.iter (fun (k, v) -> insert k v m) lst; (* n * O(n) is O(n^2) *)
    m
end
module HashMap : TableMap

An optimization of rehash is possible. When it calls insert_no_resize to re-insert a binding, extra work is being done: there’s no need for that insertion to call remove_assoc or mem_assoc, because we are guaranteed the binding does not contain a duplicate key. We could omit that work. If the hash function is good, it’s only a constant amount of work that we save. But if the hash function is bad and doesn’t distribute keys uniformly, that could be an important optimization.
rehash 的优化是可能的。当它调用 insert_no_resize 重新插入绑定时,就会完成额外的工作:该插入不需要调用 remove_assocmem_assoc ,因为我们保证绑定不包含重复的密钥。我们可以省略这项工作。如果哈希函数很好,我们只需要节省一定量的工作。但如果哈希函数很糟糕并且不能均匀地分配密钥,那么这可能是一个重要的优化。

8.1.5. Hash Functions 8.1.5. 哈希函数 ¶

Hash tables are one of the most useful data structures ever invented. Unfortunately, they are also one of the most misused. Code built using hash tables often falls far short of achievable performance. There are two reasons for this:
哈希表是有史以来发明的最有用的数据结构之一。不幸的是,它们也是最被滥用的之一。使用哈希表构建的代码通常远远达不到可实现的性能。有两个原因:

  • Clients choose poor hash functions that do not distribute keys randomly over buckets.
    客户端选择较差的哈希函数,这些函数不会在存储桶上随机分配密钥。

  • Hash table abstractions do not adequately specify what is required of the hash function, or make it difficult to provide a good hash function.
    哈希表抽象没有充分指定哈希函数的要求,或者使得难以提供良好的哈希函数。

Clearly, a bad hash function can destroy our attempts at a constant running time. A lot of obvious hash function choices are bad. For example, if we’re mapping names to phone numbers, then hashing each name to its length would be a very poor function, as would a hash function that used only the first name, or only the last name. We want our hash function to use all of the information in the key. This is a bit of an art. While hash tables are extremely effective when used well, all too often poor hash functions are used that sabotage performance.
显然,一个糟糕的哈希函数可能会破坏我们在恒定运行时间内的尝试。许多明显的哈希函数选择都是不好的。例如,如果我们将姓名映射到电话号码,那么将每个姓名散列到其长度将是一个非常糟糕的函数,就像仅使用名字或仅使用姓氏的散列函数一样。我们希望哈希函数使用密钥中的所有信息。这是一门艺术。虽然哈希表在使用得当时非常有效,但使用不良的哈希函数常常会破坏性能。

Hash tables work well when the hash function looks random. If it is to look random, this means that any change to a key, even a small one, should change the bucket index in an apparently random way. If we imagine writing the bucket index as a binary number, a small change to the key should randomly flip the bits in the bucket index. This is called information diffusion. For example, a one-bit change to the key should cause every bit in the index to flip with 1/2 probability.
当哈希函数看起来随机时,哈希表可以很好地工作。如果它看起来是随机的,这意味着对密钥的任何更改,即使是很小的更改,都应该以明显随机的方式更改存储桶索引。如果我们想象将存储桶索引写为二进制数,则对键的一个小更改应该会随机翻转存储桶索引中的位。这称为信息扩散。例如,对密钥进行一位更改应导致索引中的每一位以 1/2 的概率翻转。

Client vs. implementer. As we’ve described it, the hash function is a single function that maps from the key type to a bucket index. In practice, the hash function is the composition of two functions, one provided by the client and one by the implementer. This is because the implementer doesn’t understand the element type, the client doesn’t know how many buckets there are, and the implementer probably doesn’t trust the client to achieve diffusion.
客户 vs 实施者。正如我们所描述的,哈希函数是从键类型映射到存储桶索引的单个函数。实际上,哈希函数是两个函数的组合,一个由客户端提供,一个由实现者提供。这是因为实现者不了解元素类型,客户端不知道有多少个桶,并且实现者可能不信任客户端实现扩散。

The client function hash_c first converts the key into an integer hash code, and the implementation function hash_i converts the hash code into a bucket index. The actual hash function is the composition of these two functions. As a hash table designer, you need to figure out which of the client hash function and the implementation hash function is going to provide diffusion. If clients are sufficiently savvy, it makes sense to push the diffusion onto them, leaving the hash table implementation as simple and fast as possible. The easy way to accomplish this is to break the computation of the bucket index into three steps.
客户端函数 hash_c 首先将key转换为整数哈希码,实现函数 hash_i 将哈希码转换为桶索引。实际的哈希函数是这两个函数的组合。作为哈希表设计者,您需要弄清楚客户端哈希函数和实现哈希函数中的哪一个将提供扩散。如果客户足够精明,那么将扩散推给他们是有意义的,使哈希表实现尽可能简单和快速。实现这一点的简单方法是将桶索引的计算分为三个步骤。

  1. Serialization: Transform the key into a stream of bytes that contains all of the information in the original key. Two equal keys must result in the same byte stream. Two byte streams should be equal only if the keys are actually equal. How to do this depends on the form of the key. If the key is a string, then the stream of bytes would simply be the characters of the string.
    序列化:将密钥转换为包含原始密钥中所有信息的字节流。两个相等的键必须产生相同的字节流。仅当键实际上相等时,两个字节流才应相等。如何执行此操作取决于密钥的形式。如果键是字符串,那么字节流将只是字符串的字符。

  2. Diffusion: Map the stream of bytes into a large integer x in a way that causes every change in the stream to affect the bits of x apparently randomly. There is a tradeoff in performance versus randomness (and security) here.
    扩散:将字节流映射到一个大整数 x 中,使得流中的每个变化都会明显随机地影响 x 的位。这里需要权衡性能与随机性(和安全性)。

  3. Compression: Reduce that large integer to be within the range of the buckets. For example, compute the hash bucket index as x mod m. This is particularly cheap if m is a power of two.
    压缩:将大整数缩小到桶的范围内。例如,将哈希桶索引计算为 x mod m。如果 m 是 2 的幂,这尤其便宜。

Unfortunately, hash table implementations are rarely forthcoming about what they assume of client hash functions. So it can be hard to know, as a client, how to get good performance from a table. The more information the implementation can provide to a client about how well distributed keys are in buckets, the better.
不幸的是,哈希表的实现很少透露它们对客户端哈希函数的假设。因此,作为客户,可能很难知道如何从桌子上获得良好的性能。实现可以向客户端提供有关存储桶中密钥分布情况的信息越多越好。

8.1.6. Standard Library Hashtbl
8.1.6. 标准库 Hashtbl

Although it’s great to know how to implement a hash table, and to see how mutability is used in doing so, it’s also great not to have to implement a data structure yourself in your own projects. Fortunately the OCaml standard library does provide a module Hashtbl [sic] that implements hash tables. You can think of this module as the imperative equivalent of the functional Map module.
虽然知道如何实现哈希表以及了解如何在实现哈希表时使用可变性固然很棒,但不必在自己的项目中自己实现数据结构也很棒。幸运的是,OCaml 标准库确实提供了一个实现哈希表的模块 Hashtbl [原文如此]。您可以将此模块视为功能 Map 模块的命令式等效项。

Hash function. The function Hashtbl.hash : 'a -> int takes responsibility for serialization and diffusion. It is capable of hashing any type of value. That includes not just integers but strings, lists, trees, and so forth. So how does it run in constant time, if the length of a tree or size of a tree can be arbitrarily large? It looks only at a predetermined number of meaningful nodes of the structure it is hashing. By default, that number is 10. A meaningful node is an integer, floating-point number, string, character, booleans or constant constructor. You can see that as we hash these lists:
哈希函数。函数 Hashtbl.hash : 'a -> int 负责序列化和扩散。它能够对任何类型的值进行哈希处理。这不仅包括整数,还包括字符串、列表、树等等。那么,如果树的长度或大小可以任意大,那么它如何在恒定时间内运行呢?它仅查看其散列结构的预定数量的有意义节点。默认情况下,该数字为 10。有意义的节点是整数、浮点数、字符串、字符、布尔值或常量构造函数。当我们散列这些列表时,您可以看到:

Hashtbl.hash [1; 2; 3; 4; 5; 6; 7; 8; 9];;
Hashtbl.hash [1; 2; 3; 4; 5; 6; 7; 8; 9; 10];;
Hashtbl.hash [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11];;
Hashtbl.hash [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12];;
- : int = 635296333
- : int = 822221246
- : int = 822221246
- : int = 822221246

The hash values stop changing after the list goes beyond 10 elements. That has implications for how we use this built-in hash function: it will not necessarily provide good diffusion for large data structures, which means performance could degrade as collisions become common. To support clients who want to hash such structures, Hashtbl provides another function hash_param which can be configured to examine more nodes.
当列表超过 10 个元素后,哈希值停止变化。这对我们如何使用这个内置哈希函数有影响:它不一定能为大型数据结构提供良好的扩散,这意味着随着冲突变得普遍,性能可能会下降。为了支持想要散列此类结构的客户端, Hashtbl 提供了另一个函数 hash_param ,可以将其配置为检查更多节点。

Hash table. Here’s
哈希表。这是
an abstract of the hash table interface:
一个哈希表接口的摘要:

module type Hashtbl = struct
  type ('a, 'b) t
  val create : int -> ('a, 'b) t
  val add : ('a, 'b) t -> 'a -> 'b -> unit
  val find : ('a, 'b) t -> 'a -> 'b
  val remove : ('a, 'b) t -> 'a -> unit
  ...
end

The representation type ('a, 'b) Hashtbl.t maps keys of type 'a to values of type 'b. The create function initializes a hash table to have a given capacity, as our implementation above did. But rather than requiring the client to provide a hash function, the module uses Hashtbl.hash.
表示类型 ('a, 'b) Hashtbl.t'a 类型的键映射到 'b 类型的值。 create 函数初始化哈希表以具有给定的容量,正如我们上面的实现所做的那样。但该模块并不要求客户端提供哈希函数,而是使用 Hashtbl.hash

Resizing occurs when the load factor exceeds 2. Let’s see that happen. First, we’ll create a table and fill it up:
当负载因子超过 2 时,就会发生大小调整。让我们看看会发生什么。首先,我们将创建一个表并填充它:

open Hashtbl;;
let t = create 16;;
for i = 1 to 16 do
  add t i (string_of_int i)
done;;
val t : ('_weak1, '_weak2) Hashtbl.t = <abstr>
- : unit = ()

We can query the hash table to find out how the bindings are distributed over buckets with Hashtbl.stats:
我们可以使用 Hashtbl.stats 查询哈希表以了解绑定如何分布在存储桶上:

stats t
- : Hashtbl.statistics =
{num_bindings = 16; num_buckets = 16; max_bucket_length = 3;
 bucket_histogram = [|6; 5; 4; 1|]}

The number of bindings and number of buckets are equal, so the load factor is 1. The bucket histogram is an array a in which a.(i) is the number of buckets whose size is i.
绑定数和桶数相等,因此负载因子为1。桶直方图是一个数组 a ,其中 a.(i) 是大小为 。

Let’s pump up the load factor to 2:
让我们将负载系数提高到 2:

for i = 17 to 32 do
  add t i (string_of_int i)
done;;
stats t;;
- : unit = ()
- : Hashtbl.statistics =
{num_bindings = 32; num_buckets = 16; max_bucket_length = 4;
 bucket_histogram = [|3; 3; 3; 5; 2|]}

Now adding one more binding will trigger a resize, which doubles the number of buckets:
现在再添加一个绑定将触发调整大小,这会使存储桶的数量加倍:

add t 33 "33";;
stats t;;
- : unit = ()
- : Hashtbl.statistics =
{num_bindings = 33; num_buckets = 32; max_bucket_length = 3;
 bucket_histogram = [|11; 11; 8; 2|]}

But Hashtbl does not implement resize on removal:
Hashtbl 不会在删除时实现调整大小:

for i = 1 to 33 do
  remove t i
done;;
stats t;;
- : unit = ()
- : Hashtbl.statistics =
{num_bindings = 0; num_buckets = 32; max_bucket_length = 0;
 bucket_histogram = [|32|]}

The number of buckets is still 32, even though all bindings have been removed.
尽管所有绑定已被删除,但存储桶的数量仍然是 32。

Note 笔记

Java’s HashMap has a default constructor HashMap() that creates an empty hash table with a capacity of 16 that resizes when the load factor exceeds 0.75 rather than 2. So Java hash tables would tend to have a shorter bucket length than OCaml hash tables, but also would tend to take more space to store because of empty buckets.
Java 的 HashMap 有一个默认构造函数 HashMap() ,它创建一个容量为 16 的空哈希表,当负载因子超过 0.75 而不是 2 时,它会调整大小。因此 Java 哈希表往往具有桶长度比 OCaml 哈希表更短,但由于空桶,往往会占用更多空间来存储。

Client-provided hash functions. What if a client of Hashtbl found that the default hash function was leading to collisions, hence poor performance? Then it would make sense to change to a different hash function. To support that, Hashtbl provides a functorial interface similar to Map. The functor is Hashtbl.Make, and it requires an input of the following module type:
客户端提供的哈希函数。如果 Hashtbl 的客户端发现默认哈希函数导致冲突,从而导致性能不佳怎么办?那么更改为不同的哈希函数就有意义了。为了支持这一点, Hashtbl 提供了一个类似于 Map 的函数接口。函子是 Hashtbl.Make ,它需要以下模块类型的输入:

module type HashedType = sig
  type t
  val equal : t -> t -> bool
  val hash : t -> int
end

Type t is the key type for the table, and the two functions equal and hash say how to compare keys for equality and how to hash them. If two keys are equal according to equal, they must have the same hash value according to hash. If that requirement were violated, the hash table would no longer operate correctly. For example, suppose that equal k1 k2 holds but hash k1 <> hash k2. Then k1 and k2 would be stored in different buckets. So if a client added a binding of k1 to v, then looked up k2, they would not get v back.
类型 t 是表的键类型,两个函数 equalhash 说明如何比较键是否相等以及如何对它们进行哈希处理。如果根据 equal 两个键相等,则根据 hash ,它们必须具有相同的哈希值。如果违反该要求,哈希表将无法正常运行。例如,假设 equal k1 k2 成立,但 hash k1 <> hash k2 成立。那么 k1k2 将被存储在不同的桶中。因此,如果客户端将 k1 的绑定添加到 v ,然后查找 k2 ,他们将不会得到 v 返回。

Note 笔记

That final requirement might sound familiar from Java. There, if you override Object.equals() and Object.hashCode() you must ensure the same correspondence.
最后一个要求对于 Java 来说可能听起来很熟悉。在那里,如果您覆盖 Object.equals()Object.hashCode() ,则必须确保相同的对应关系。

8.2. Amortized Analysis
8.2. 摊销分析 ¶

Our analysis of the efficiency of hash table operations concluded that find runs in expected constant time, where the modifier “expected” is needed to express the fact the performance is on average and depends on the hash function satisfying certain properties.
我们对哈希表操作效率的分析得出的结论是 find 在预期恒定时间内运行,其中需要修饰符“expected”来表达性能是平均的这一事实,并且取决于满足某些属性的哈希函数。

We also concluded that insert would usually run in expected constant time, but that in the worst case it would require linear time because of needing to rehash the entire table. That kind of defeats the goal of a hash table, which is to offer constant-time performance, or at least as close to it as we can get.
我们还得出结论, insert 通常会在预期的恒定时间内运行,但在最坏的情况下,由于需要重新散列整个表,因此需要线性时间。这违背了哈希表的目标,即提供恒定时间性能,或者至少尽可能接近它。

It turns out there is another way of looking at this analysis that allows us to conclude that insert does have “amortized” expected constant time performance—that is, for excusing the occasional worst-case linear performance. Right away, we have to acknowledge this technique is just a change in perspective. We’re not going to change the underlying algorithms. The insert algorithm will still have worst-case linear performance. That’s a fact.
事实证明,还有另一种看待此分析的方式,使我们能够得出结论, insert 确实具有“摊销”预期恒定时间性能,即,可以原谅偶尔出现的最坏情况线性性能。我们必须立即承认这种技术只是视角的改变。我们不会改变底层算法。 insert 算法仍将具有最坏情况的线性性能。这是事实。

But the change in perspective we now undertake is to recognize that if it’s very rare for insert to require linear time, then maybe we can “spread out” that cost over all the other calls to insert. It’s a creative accounting trick!
但我们现在采取的观点转变是认识到,如果 insert 需要线性时间的情况非常罕见,那么也许我们可以将该成本“分散”到所有其他对 insert 的调用上。这是一个创造性的会计技巧!

Sushi vs. Ramen. Let’s amuse ourselves with a real-world example for a moment. Suppose that you have $20 to spend on lunches for the week. You like to eat sushi, but you can’t afford to have sushi every day. So instead you eat as follows:
寿司 vs 拉面。让我们用一个现实世界的例子来娱乐一下。假设您有 20 美元可用于一周的午餐。你喜欢吃寿司,但你不可能每天都吃寿司。所以你应该按如下方式吃:

  • Monday: $1 ramen 周一:1 美元拉面

  • Tuesday: $1 ramen 周二:1 美元拉面

  • Wednesday: $1 ramen 周三:1 美元拉面

  • Thursday: $1 ramen 周四:1 美元拉面

  • Friday: $16 sushi 周五:寿司 16 美元

Most of the time, your lunch was cheap. On a rare occasion, it was expensive. So you could look at it in one of two ways:
大多数时候,你的午餐很便宜。在极少数情况下,它很昂贵。因此,您可以通过以下两种方式之一来查看它:

  • My worst-case lunch cost was $16.
    我最坏情况下的午餐费用是 16 美元。

  • My average lunch cost was $4.
    我的平均午餐费用是 4 美元。

Both are true statements, but maybe the latter is more helpful in understanding your spending habits.
两者都是正确的说法,但也许后者更有助于了解您的消费习惯。

Back to Hash Tables. It’s the same with hash tables. Even though insert is occasionally expensive, it’s so rarely expensive that the average cost of an operation is actually constant time! But, we need to do more complicated math (or more complicated than our lunch budgeting anyway) to actually demonstrate that’s true.
回到哈希表。哈希表也是如此。尽管 insert 偶尔会很昂贵,但它很少很昂贵,以至于操作的平均成本实际上是常数时间!但是,我们需要做更复杂的数学(或者比我们的午餐预算更复杂)来实际证明这是真的。

8.2.1. Amortized Analysis of Hash Tables
8.2.1. 哈希表的摊销分析 ¶

“Amortization” is a financial term. One of its meanings is to pay off a debt over time. In algorithmic analysis, we use it to refer to paying off the cost of an expensive operation by inflating the cost of inexpensive operations. In effect, we pre-pay the cost of a later expensive operation by adding some additional cost to earlier cheap operations.
“摊销”是一个财务术语。其含义之一是随着时间的推移还清债务。在算法分析中,我们用它来指通过夸大廉价操作的成本来偿还昂贵操作的成本。实际上,我们通过在早期廉价操作中添加一些额外成本来预先支付后来昂贵操作的成本。

The amortized complexity or amortized running time of a sequence of operations that each have cost T1,T2,,Tn, is just the average cost of each operation:
每个操作都有成本 T1,T2,,Tn 的摊余复杂度或摊销运行时间只是每个操作的平均成本:

T1+T2+...+Tnn.

Thus, even if one operation is especially expensive, we could average that out over a bunch of inexpensive operations.
因此,即使一个操作特别昂贵,我们也可以将其平均到一堆便宜的操作上。

Applying that idea to a hash table, suppose the table has 8 bindings and 8 buckets. Then 8 more inserts are made. The first 7 are (expected) constant-time, but the 8th insert is linear time: it increases the load factor to 2, causing a resize, thus causing rehashing of all 16 bindings into a new table. The total cost over that series of operations is therefore the cost of 8+16 inserts. For simplicity of calculation, we could grossly round that up to 16+16 = 32 inserts. So the average cost of each operation in the sequence is 32/8 = 4 inserts.
将这个想法应用到哈希表中,假设该表有 8 个绑定和 8 个存储桶。然后再制作 8 个刀片。前 7 个是(预期的)恒定时间,但第 8 个插入是线性时间:它将负载因子增加到 2,导致调整大小,从而导致所有 16 个绑定重新散列到新表中。因此,该系列操作的总成本是 8+16 次插入的成本。为了简化计算,我们可以将其大致舍入为 16+16 = 32 个插入。因此序列中每个操作的平均成本为 32/8 = 4 次插入。

In other words, if we just pretended each insert cost four times its normal price, the final operation in the sequence would have been “pre-paid” by the extra price we paid for earlier inserts. And all of them would be constant-time, since four times a constant is still a constant.
换句话说,如果我们假设每个插入的成本是正常价格的四倍,那么序列中的最终操作将由我们为早期插入支付的额外价格“预付”。所有这些都是常数时间,因为常数的四倍仍然是常数。

Generalizing from the example above, let’s suppose that the the number of buckets currently in a hash table is 2n, and that the load factor is currently 1. Therefore, there are currently 2n bindings in the table. Next:
从上面的示例进行概括,我们假设哈希表中当前的存储桶数量为 2n ,并且负载因子当前为 1。因此,当前有 2n 绑定在表中。下一个:

  • A series of 2n1 inserts occurs. There are now 2n+2n1 bindings in the table.
    发生一系列 2n1 插入。表中现在有 2n+2n1 绑定。

  • One more insert occurs. That brings the number of bindings up to 2n+2n, which is 2n+1. But the number of buckets is 2n, so the the load factor just reached 2. A resize is necessary.
    又发生一次插入。这使得绑定数量达到 2n+2n ,即 2n+1 。但是桶的数量是 2n ,因此负载因子刚刚达到 2。需要调整大小。

  • The resize occurs. That doubles the number of buckets. All 2n+1 bindings have to be reinserted into the new table, which is of size 2n+1. The load factor is back down to 1.
    发生调整大小。这使得桶的数量增加了一倍。所有 2n+1 绑定都必须重新插入到大小为 2n+1 的新表中。负载系数回到 1。

So in total we did 2n+2n+1 inserts, which included 2n inserts of bindings and 2n+1 re-insertions after the resize. We could grossly round that quantity up to 2n+2. Over a series of 2n insert operations, that’s an average cost of 2n+22n, which equals 4. So if we just pretend each insert costs four times its normal price, every operation in the sequence is amortized (and expected) constant time.
因此,我们总共进行了 2n+2n+1 插入,其中包括 2n 绑定插入和调整大小后的 2n+1 重新插入。我们可以将该数量大致舍入到 2n+2 。在一系列 2n 插入操作中,平均成本为 2n+22n ,等于 4。因此,如果我们假设每次插入的成本是正常价格的四倍,则序列中的每个操作是摊销(和预期)常数时间。

Doubling vs. Constant-size Increasing. Notice that it is crucial that the array size grows by doubling (or at least geometrically). A bad mistake would be to instead grow the array by a fixed increment—for example, 100 buckets at time. Then we’d be in real trouble as the number of bindings continued to grow:
加倍与恒定大小增加。请注意,数组大小增加一倍(或至少呈几何倍数)至关重要。一个严重的错误是按固定增量增长数组,例如一次增长 100 个桶。然后,随着绑定数量的持续增长,我们就会遇到真正的麻烦:

  • Start with 100 buckets and 100 bindings. The load factor is 1.
    从 100 个桶和 100 个绑定开始。负载系数为1。

  • Round 1. Insert 100 bindings. There are now 200 bindings and 100 buckets. The load factor is 2.
    第 1 轮。插入 100 个绑定。现在有 200 个绑定器和 100 个桶。负载系数为2。

  • Increase the number of buckets by 100 and rehash. That’s 200 more insertions. The load factor is back down to 1.
    将存储桶数量增加 100 并重新散列。还有 200 个插入。负载系数回到 1。

  • The average cost of each insert is so far just 3x the cost of an actual insert (100+200 insertions / 100 bindings inserted). So far so good.
    到目前为止,每个插入的平均成本仅为实际插入成本的 3 倍(100+200 个插入/插入的 100 个绑定)。到目前为止,一切都很好。

  • Round 2. Insert 200 more bindings. There are now 400 bindings and 200 buckets. The load factor is 2.
    第 2 轮。再插入 200 个绑定。现在有 400 个绑定器和 200 个桶。负载系数为2。

  • Increase the number of buckets by 100 and rehash. That’s 400 more insertions. There are now 400 bindings and 300 buckets. The load factor is 400/300 = 4/3, not 1.
    将存储桶数量增加 100 并重新散列。还有 400 个插入。现在有 400 个绑定器和 300 个桶。负载系数为 400/300 = 4/3,而不是 1。

  • The average cost of each insert is now 100+200+200+400 / 300 = 3. That’s still okay.
    现在每个插入的平均成本是 100+200+200+400 / 300 = 3。这仍然可以。

  • Round 3. Insert 200 more bindings. There are now 600 bindings and 300 buckets. The load factor is 2.
    第 3 轮。再插入 200 个绑定。现在有 600 个绑定器和 300 个桶。负载系数为2。

  • Increase the number of buckets by 100 and rehash. That’s 600 more insertions. There are now 600 bindings and 400 buckets. The load factor is 3/2, not 1.
    将存储桶数量增加 100 并重新散列。还有 600 个插入。现在有 600 个绑定器和 400 个桶。负载系数为 3/2,而不是 1。

  • The average cost of each insert is now 100+200+200+400+200+600 / 500 = 3.4. It’s going up.
    现在每个插入件的平均成本为 100+200+200+400+200+600 / 500 = 3.4。它正在上升。

  • Round 4. Insert 200 more bindings. There are now 800 bindings and 400 buckets. The load factor is 2.
    第 4 轮。再插入 200 个绑定。现在有 800 个绑定器和 400 个桶。负载系数为2。

  • Increase the number of buckets by 100 and rehash. That’s 800 more insertions. There are now 800 bindings and 500 buckets. The load factor is 8/5, not 1.
    将存储桶数量增加 100 并重新散列。还有 800 个插入。现在有800个绑定器和500个桶。负载系数是 8/5,而不是 1。

  • The average cost of each insert is now 100+200+200+400+200+600+200+800 / 700 = 3.9. It’s continuing to go up, not staying constant.
    现在每个插入件的平均成本为 100+200+200+400+200+600+200+800 / 700 = 3.9。它会持续上升,而不是保持不变。

After k rounds we have 200k bindings and 100k buckets. We have called insert to insert 100+200k bindings, but all the rehashing has caused us to do 100+200(k1)+i=1k200i actual insertions. That last term is the real problem. It’s quadratic:
k 轮之后,我们有 200k 绑定和 100k 存储桶。我们已调用 insert 来插入 100+200k 绑定,但所有重新哈希导致我们执行 100+200(k1)+i=1k200i 实际插入。最后一个术语才是真正的问题。它是二次方的:

i=1k200i=200k(200(k+1))2=20,000(k2+k)

So over a series of n calls to insert, we do O(n2) actual inserts. That makes the amortized cost of insert be O(n), which is linear! Not constant.
因此,通过对 insert 的一系列 n 调用,我们执行 O(n2) 实际插入。这使得 insert 的摊余成本为 O(n) ,这是线性的!不是恒定的。

That’s why it’s so important to double the size of the array at each rehash. It’s what gives us the amortized constant-time performance.
这就是为什么在每次重新哈希时将数组大小加倍如此重要。这给了我们摊余的恒定时间性能。

8.2.2. Amortized Analysis of Batched Queues
8.2.2. 批量队列的摊销分析 ¶

The implementation of batched queues with two lists was in a way more efficient than the implementation with just one list, because it managed to achieve a constant time enqueue operation. But, that came at the tradeoff of making the dequeue operation sometimes take more than constant time: whenever the outbox became empty, the inbox had to be reversed, which required an additional linear-time operation.
具有两个列表的批处理队列的实现在某种程度上比仅具有一个列表的实现更高效,因为它成功地实现了恒定时间 enqueue 操作。但是,这样做的代价是 dequeue 操作有时需要比恒定时间更长的时间:每当发件箱变空时,收件箱就必须反转,这需要额外的线性时间操作。

As we observed then, the reversal is relatively rare. It happens only when the outbox gets exhausted. Amortized analysis gives us a way to account for that. We can actually show that the dequeue operation is amortized constant time.
正如我们当时观察到的那样,这种逆转相对罕见。仅当发件箱耗尽时才会发生这种情况。摊销分析为我们提供了一种解释这一点的方法。我们实际上可以证明 dequeue 操作是摊销常数时间的。

To keep the analysis simple at first, let’s assume the queue starts off with exactly one element 1 already enqueued, and that we do three enqueue operations of 2, 3, then 4, followed by a single dequeue. The single initial element would end up in the outbox. All three enqueue operations would cons an element onto the inbox. So just before the dequeue, the queue looks like:
为了让分析一开始就简单,我们假设队列一开始就已经有一个元素 1 入队,并且我们对 2 执行了三个 enqueue 操作, 3 ,然后是 4 ,最后是一个 dequeue 。单个初始元素将最终出现在发件箱中。所有三个 enqueue 操作都会将一个元素添加到收件箱中。因此,在 dequeue 之前,队列如下所示:

{o = [1]; i = [4; 3; 2]}

and after the dequeue:
dequeue 之后:

{o = [2; 3; 4]; i = []}

It required 它需要

  • 3 cons operations to do the 3 enqueues, and
    3 个缺点操作来执行 3 个队列,以及

  • another 3 cons operations to finish the dequeue by reversing the list.
    另外 3 个缺点操作是通过反转列表来完成出队。

That’s a total of 6 cons operations to do the 4 enqueue and dequeue operations. The average cost is therefore 1.5 cons operations per queue operation. There were other pattern matching operations and record constructions, but those all took only constant time, so we’ll ignore them.
总共需要 6 个 cons 操作来执行 4 个 enqueuedequeue 操作。因此,每个队列操作的平均成本为 1.5 个 cons 操作。还有其他模式匹配操作和记录构造,但这些都只花费恒定的时间,所以我们将忽略它们。

What about a more complicated situation, where there are enqueues and dequeues interspersed with one another? Trying to take averages over the series is going to be tricky to analyze. But, inspired by our analysis of hash tables, suppose we pretend that the cost of each enqueue is twice its actual cost, as measured in cons operations? Then at the time an element is enqueued, we could “prepay” the later cost that will be incurred when that element is cons’d onto the reversed list.
如果 enqueuesdequeues 相互散布,那么更复杂的情况又如何呢?尝试取整个系列的平均值将很难进行分析。但是,受我们对哈希表分析的启发,假设我们假设每个 enqueue 的成本是其实际成本的两倍(在 cons 操作中测量)?然后,当一个元素入队时,我们可以“预付”当该元素被添加到反向列表中时将产生的后续成本。

The enqueue operation is still constant time, because even though we’re now pretending its cost is 2 instead of 1, it’s still the case that 2 is a constant. And the dequeue operation is amortized constant time:
enqueue 操作仍然是常数时间,因为即使我们现在假设它的成本是 2 而不是 1,但 2 仍然是常数。 dequeue 操作是摊销常数时间:

  • If dequeue doesn’t need to reverse the inbox, it really does just constant work, and
    如果 dequeue 不需要反转收件箱,它实际上只是持续工作,并且

  • If dequeue does need to reverse an inbox with n elements, it already has n units of work “saved up” from each of the enqueues of those n elements.
    如果 dequeue 确实需要反转包含 n 元素的收件箱,则它已经从这些 n 个工作单元> 元素。

So if we just pretend each enqueue costs twice its normal price, every operation in a sequence is amortized constant time. Is this just a bookkeeping trick? Absolutely. But it also reveals the deeper truth that on average we get constant-time performance, even though some operations might rarely have worst-case linear-time performance.
因此,如果我们假设每个队列的成本是正常价格的两倍,则序列中的每个操作都会摊销恒定时间。这只是记账技巧吗?绝对地。但它也揭示了更深层次的事实,即平均而言,我们获得了恒定时间性能,即使某些操作可能很少具有最坏情况的线性时间性能。

8.2.3. Bankers and Physicists
8.2.3. 银行家和物理学家 ¶

Conceptually, amortized analysis can be understood in three ways:
从概念上讲,摊销分析可以通过三种方式来理解:

  1. Taking the average cost over a series of operations. This is what we’ve done so far.
    取一系列操作的平均成本。这就是我们到目前为止所做的。

  2. Keeping a “bank account” at each individual element of a data structure. Some operations deposit credits, and others withdraw them. The goal is for account totals to never be negative. The amortized cost of any operation is the actual cost, plus any credits deposited, minus any credits spent. So if an operation actually costs n but spends n1 credits, then its amortized cost is just 1. This is called the banker’s method of amortized analysis.
    在数据结构的每个单独元素上保留一个“银行账户”。一些操作存入积分,而另一些操作则提取积分。目标是账户总数永远不会为负。任何操作的摊余成本是实际成本加上任何存入的积分,减去任何花费的积分。因此,如果一项操作实际成本 n 但花费 n1 积分,则其摊余成本仅为 1 。这称为银行家摊销分析法。

  3. Regarding the entire data structure as having an amount of “potential energy” stored up. Some operations increase the energy, some decrease it. The energy should never be negative. The amortized cost of any operation is its actual cost, plus the change in potential energy. So if an operation actually costs n, and before the operation the potential energy is n, and after the operation the potential energy is 0, then the amortized cost is n+(0n), which is just 0. This is called the physicist’s method of amortized analysis.
    将整个数据结构视为存储了一定量的“势能”。有些操作会增加能量,有些则会减少能量。能量永远不应该是负的。任何操作的摊余成本是其实际成本加上势能的变化。因此,如果操作实际成本 n ,并且操作之前的势能是 n ,操作之后的势能是 0 ,那么摊余成本是 n+(0n) ,也就是 0 。这称为物理学家的摊销分析法。

The banker’s and physicist’s methods can be easier to use in many situations than a complicated analysis of a series of operations. Let’s revisit our examples so far to illustrate their use:
在许多情况下,银行家和物理学家的方法比一系列操作的复杂分析更容易使用。让我们回顾一下到目前为止的示例来说明它们的用法:

  • Banker’s method, hash tables: The table starts off empty. When a binding is added to the table, save up 1 credit in its account. When a rehash becomes necessary, every binding is guaranteed to have 1 credit. Use that credit to pay for the rehash. Now all bindings have 0 credits. From now on, when a binding is added to the table, save up 1 credit in its account and 1 credit in the account of any one of the bindings that has 0 credits. At the time the next rehash becomes necessary, the number of bindings has doubled. But since we’ve saved 2 credits at each insertion, every binding now has 1 credit in its account again. So we can pay for the rehash. The accounts never go negative, because they always have either 0 or 1 credit.
    银行家的方法,哈希表:表开始时是空的。当绑定添加到表中时,在其帐户中保存 1 个积分。当需要重新哈希时,每个绑定都保证有 1 个积分。使用该积分来支付重新哈希的费用。现在所有绑定的积分均为 0。从现在开始,当绑定添加到表中时,在其帐户中保存 1 个积分,并在任何一个积分为 0 的绑定的帐户中保存 1 个积分。当需要进行下一次重新哈希时,绑定数量已增加一倍。但由于我们在每次插入时都节省了 2 个积分,因此每个绑定的帐户中现在又拥有 1 个积分。这样我们就可以支付重新哈希的费用。账户永远不会出现负数,因为它们的贷方总是 0 或 1。

  • Banker’s method, batched queues: When an element is added to the queue, save up 1 credit in its account. When the inbox must be reversed, use the credit in each element to pay for the cons onto the outbox. Since elements enter at the inbox and transition at most once to the outbox, every element will have 0 or 1 credits. So the accounts never go negative.
    银行家方法,批量队列:当一个元素被添加到队列中时,在其帐户中保存 1 个积分。当必须反转收件箱时,请使用每个元素中的积分来支付发件箱中的缺点。由于元素进入收件箱并最多转换一次到发件箱,因此每个元素将有 0 或 1 个积分。所以账目永远不会变成负数。

  • Physicist’s method, hash tables: At first, define the potential energy of the table to be the number of bindings inserted. That energy will therefore never be negative. Each insertion increases the energy by 1 unit. When the first rehash is needed after inserting n bindings, the potential energy is n. The potential goes back down to 0 at the rehash. So the actual cost is n, but the change in potential is n, which makes the amortized cost 0, or constant. From now on, define the potential energy to be twice the number of bindings inserted since the last rehash. Again, the energy will never be negative. Each insertion increases the energy by 2 units. When the next rehash is needed after inserting n bindings, there will be 2n bindings that need to be rehashed. Again, the amortized cost will be constant, because the actual cost of 2n re-insertions is offset by the 2n change in potential.
    物理学家的方法,哈希表:首先,将表的势能定义为插入的绑定数。因此,这种能量永远不会是负的。每次插入都会使能量增加 1 个单位。当插入 n 绑定后需要第一次重新哈希时,势能为 n 。重新散列时,电位会回到 0 。因此,实际成本为 n ,但潜在变化为 n ,这使得摊余成本 0 或恒定。从现在开始,将势能定义为自上次重新哈希以来插入的绑定数量的两倍。再说一次,能量永远不会是负的。每次插入都会使能量增加 2 个单位。当插入 n 绑定后需要进行下一次重新哈希时,将会有 2n 绑定需要重新哈希。同样,摊余成本将是恒定的,因为 2n 重新插入的实际成本被 2n 潜在变化所抵消。

  • Physicist’s method, batched queues: Define the potential energy of the queue to be the length of the inbox. It therefore will never be negative. When a dequeue has to reverse an inbox of length n, there is an actual cost of n but a change in potential of n too, which offsets the cost and makes it constant.
    物理学家的方法,批量队列:将队列的势能定义为收件箱的长度。因此它永远不会是负数。当 dequeue 必须反转长度为 n 的收件箱时,实际成本为 n ,但潜在变化为 n 这也抵消了成本并使其保持不变。

The two methods are equivalent in their analytical power:
这两种方法的分析能力是等效的:

  • To convert a banker’s analysis into a physicist’s, just make the potential be the sum of all the credits in the individual accounts.
    要将银行家的分析转换为物理学家的分析,只需将潜力设为个人账户中所有学分的总和即可。

  • To convert a physicist’s analysis into a banker’s, just designate one distinguished element of the data structure to be the only one that will ever hold any credits, and have each operation deposit or withdraw the change in potential into that element’s account.
    要将物理学家的分析转换为银行家的分析,只需将数据结构的一个显着元素指定为唯一持有任何积分的元素,并让每次操作将势能变化存入或提取到该元素的账户中。

So, the choice of which to use really just depends on which is easier for the data structure being analyzed, or which is easier for you to wrap your head around. You might find one or the other of the methods easier to understand for the data structures above, and your friend might have a different opinion.
因此,选择使用哪个实际上只取决于哪个更容易分析数据结构,或者哪个更容易让您理解。您可能会发现上述数据结构中的一种或另一种方法更容易理解,而您的朋友可能有不同的看法。

8.2.4. Amortized Analysis and Persistence
8.2.4. 摊销分析和持久性 ¶

Amortized analysis breaks down as a technique when data structures are used persistently. For example, suppose we have a batched queue q into which we’ve inserted n+1 elements. One element will be in the outbox, and the other n will be in the inbox. Now we do the following:
当数据结构被持续使用时,摊销分析就不再是一种技术。例如,假设我们有一个批处理队列 q ,我们已在其中插入了 n+1 元素。一个元素将位于发件箱中,另一个 n 将位于收件箱中。现在我们执行以下操作:

# let q1 = dequeue q
# let q2 = dequeue q
...
# let qn = dequeue q

Each one of those n dequeue operations requires an actual cost of O(n) to reverse the inbox. So the entire series has an actual cost of O(n2). But the amortized analysis techniques only apply to the first dequeue. After that, all the the accounts are empty (banker’s method), or the potential is zero (physicist’s), which means the remaining operations can’t use them to pay for the expensive list reversal. The total cost of the series is therefore O(n2n), which is O(n2).
其中每一项 n dequeue 操作都需要 O(n) 的实际成本来反转收件箱。所以整个系列的实际成本为 O(n2) 。但摊销分析技术仅适用于第一个 dequeue 。之后,所有账户都为空(银行家方法),或者潜力为零(物理学家方法),这意味着剩余的操作无法使用它们来支付昂贵的列表反转。因此,该系列的总成本为 O(n2n) ,即 O(n2)

The problem with persistence is that it violates the assumption built-in to amortized analysis that credits (or energy units) are spent only once. Every persistent copy of the data structure instead tries to spend them itself, not being aware of all the other copies.
持久性的问题在于,它违反了摊销分析中内置的假设,即信用(或能量单位)仅花费一次。相反,数据结构的每个持久副本都会尝试自行消耗它们,而不知道所有其他副本。

There are more advanced techniques for amortized analysis that can account for persistence. Those techniques are based on the idea of accumulating debt that is later paid off, rather than accumulating savings that are later spent. The reason that debt ends up working as an analysis technique can be summed up as: although our banks would never (financially speaking) allow us to spend money twice, they would be fine with us paying off our debt multiple times. Consult Okasaki’s Purely Functional Data Structures to learn more.
有更先进的摊销分析技术可以解释持久性。这些技术的理念是积累随后还清的债务,而不是积累随后花掉的储蓄。债务最终成为一种分析技术的原因可以概括为:虽然我们的银行(从财务上来说)永远不会允许我们花两次钱,但他们会同意我们多次偿还债务。请参阅冈崎的纯函数式数据结构以了解更多信息。

8.3. Red-Black Trees 8.3. 红黑树 ¶

As we’ve now seen, hash tables are an efficient data structure for implementing a map ADT. They offer amortized, expected constant-time performance—which is a subtle guarantee because of those “amortized” and “expected” qualifiers we have to add. Hash tables also require mutability to implement. As functional programmers, we prefer to avoid mutability when possible.
正如我们现在所看到的,哈希表是实现映射 ADT 的有效数据结构。它们提供摊销的、预期的恒定时间性能——这是一个微妙的保证,因为我们必须添加那些“摊销”和“预期”限定符。哈希表还需要可变性来实现。作为函数式程序员,我们更愿意尽可能避免可变性。

So, let’s investigate how to implement functional maps. One of the best data structures for that is the red-black tree, which is a kind of balanced binary search tree that offers worst-case logarithmic performance. So on one hand the performance is somewhat worse than hash tables (logarithmic vs. constant), but on the other hand we don’t have to qualify the performance with words like “amortized” and “expected”. Logarithmic is actually still plenty efficient for even very large workloads. And, we get to avoid mutability!
那么,让我们研究一下如何实现功能图。最好的数据结构之一是红黑树,它是一种平衡二叉搜索树,可提供最坏情况下的对数性能。因此,一方面,性能比哈希表(对数与常数)要差一些,但另一方面,我们不必用“摊销”和“预期”等词来限定性能。实际上,即使对于非常大的工作负载,对数仍然非常高效。而且,我们可以避免可变性!

8.3.1. Binary Search Trees
8.3.1. 二叉搜索树 ¶

A binary search tree (BST) is a binary tree with the following representation invariant:
二叉搜索树 (BST) 是具有以下表示不变式的二叉树:

For any node n, every node in the left subtree of n has a value less than n’s value, and every node in the right subtree of n has a value greater than n’s value.
对于任意节点n,n的左子树中的每个节点的值都小于n的值,并且n的右子树中的每个节点的值都大于n的值。

We call that the BST invariant.
我们称之为 BST 不变量

Here is code that implements a couple of operations on a BST:
下面是在 BST 上实现几个操作的代码:

type 'a tree = Node of 'a * 'a tree * 'a tree | Leaf

(** [mem x t] is [true] iff [x] is a member of [t]. *)
let rec mem x = function
  | Leaf -> false
  | Node (y, l, r) ->
    if x < y then mem x l
    else if x > y then mem x r
    else true

(** [insert x t] is [t] . *)
let rec insert x = function
  | Leaf -> Node (x, Leaf, Leaf)
  | Node (y, l, r) as t ->
    if x < y then Node (y, insert x l, r)
    else if x > y then Node (y, l, insert x r)
    else t
type 'a tree = Node of 'a * 'a tree * 'a tree | Leaf
val mem : 'a -> 'a tree -> bool = <fun>
val insert : 'a -> 'a tree -> 'a tree = <fun>

What is the running time of those operations? Since insert is just a mem with an extra constant-time node creation, we focus on the mem operation.
这些操作的运行时间是多少?由于 insert 只是一个具有额外的恒定时间节点创建的 mem ,因此我们重点关注 mem 操作。

The running time of mem is O(h), where h is the height of the tree, because every recursive call descends one level in the tree. What’s the worst-case height of a tree? It occurs with a tree of n nodes all in a single long branch—imagine adding the numbers 1,2,3,4,5,6,7 in order into the tree. So the worst-case running time of mem is still O(n), where n is the number of nodes in the tree.
mem 的运行时间是 O(h) ,其中 h 是树的高度,因为每个递归调用都会在树中下降一层。最坏情况下树的高度是多少?它发生在一个由 n 节点组成的树中,所有节点都位于一个长分支中 - 想象一下将数字 1,2,3,4,5,6,7 按顺序添加到树中。因此 mem 最坏情况的运行时间仍然是 O(n) ,其中 n 是树中的节点数。

What is a good shape for a tree that would allow for fast lookup? A perfect binary tree has the largest number of nodes n for a given height h, which is n=2h+11. Therefore h=log(n+1)1, which is O(logn).
对于允许快速查找的树来说,什么是好的形状?对于给定高度 h ,完美二叉树具有最大数量的节点 n ,即 n=2h+11 。因此 h=log(n+1)1 ,即 O(logn)

If a tree with n nodes is kept balanced, its height is O(logn), which leads to a lookup operation running in time O(logn).
如果具有 n 节点的树保持平衡,则其高度为 O(logn) ,这会导致查找操作在 O(logn) 时间内运行。

How can we keep a tree balanced? It can become unbalanced during element insertion or deletion. Most balanced tree schemes involve adding or deleting an element just like in a normal binary search tree, followed by some kind of tree surgery to rebalance the tree. Some examples of balanced binary search tree data structures include:
我们怎样才能保持树的平衡呢?在元素插入或删除过程中它可能会变得不平衡。大多数平衡树方案涉及像普通二叉搜索树一样添加或删除元素,然后进行某种树手术来重新平衡树。平衡二叉搜索树数据结构的一些示例包括:

  • AVL trees (1962) AVL 树 (1962)

  • 2-3 trees (1970’s) 2-3 棵树(1970 年代)

  • Red-black trees (1970’s) 红黑树(1970 年代)

Each of these ensures O(logn) running time by enforcing a stronger invariant on the data structure than just the binary search tree invariant.
其中每一个都通过在数据结构上强制执行比二叉搜索树不变量更强的不变量来确保 O(logn) 运行时间。

8.3.2. Red-Black Trees 8.3.2. 红黑树 ¶

Red-black trees are relatively simple balanced binary tree data structure. The idea is to strengthen the representation invariant so a tree has height logarithmic in the number of nodes n. To help enforce the invariant, we color each node of the tree either red or black. Where it matters, we consider the color of an empty tree to be black.
红黑树是比较简单的平衡二叉树数据结构。这个想法是加强表示不变性,使树的高度与节点数 n 成对数。为了帮助强制不变量,我们将树的每个节点着色为红色或黑色。在重要的地方,我们认为空树的颜色是黑色。

type color = Red | Black
type 'a rbtree = Leaf | Node of color * 'a * 'a rbtree * 'a rbtree
type color = Red | Black
type 'a rbtree = Leaf | Node of color * 'a * 'a rbtree * 'a rbtree

Here are the new conditions we add to the binary search tree representation invariant:
以下是我们添加到二叉搜索树表示不变量中的新条件:

  1. Local Invariant: There are no two adjacent red nodes along any path.
    局部不变式:任何路径上都不存在两个相邻的红色节点。

  2. Global Invariant: Every path from the root to a leaf has the same number of black nodes. This number is called the black height (BH) of the tree.
    全局不变性:从根到叶子的每条路径都有相同数量的黑色节点。这个数字称为树的黑高(BH)。

If a tree satisfies these two conditions, it must also be the case that every subtree of the tree also satisfies the conditions. If a subtree violated either of the conditions, the whole tree would also.
如果一棵树满足这两个条件,那么该树的每个子树也必须满足这两个条件。如果子树违反了任一条件,则整棵树也会违反。

Additionally, by convention the root of the tree is colored black. This does not violate the invariants, but it also is not required by them.
此外,按照惯例,树的根部是黑色的。这并不违反不变量,但也不是它们所要求的。

With these invariants, the longest possible path from the root to an empty node would alternately contain red and black nodes; therefore it is at most twice as long as the shortest possible path, which only contains black nodes. The longest path cannot have a length greater than twice the length of the paths in a perfect binary tree, which is O(logn). Therefore, the tree has height O(logn) and the operations are all asymptotically logarithmic in the number of nodes.
有了这些不变量,从根到空节点的最长可能路径将交替包含红色和黑色节点;因此它最多是最短路径(仅包含黑色节点)的两倍长。最长路径的长度不能大于完美二叉树中路径长度的两倍,即 O(logn) 。因此,树的高度 O(logn) 并且操作都在节点数上渐近对数。

How do we check for membership in red-black trees? Exactly the same way as for general binary trees.
我们如何检查红黑树的成员资格?与一般二叉树完全相同。

let rec mem x = function
  | Leaf -> false
  | Node (_, y, l, r) ->
    if x < y then mem x l
    else if x > y then mem x r
    else true
val mem : 'a -> 'a rbtree -> bool = <fun>

Okasaki’s Algorithm. More interesting is the insert operation. As with standard binary trees, we add a node by replacing the leaf found by the search procedure. But what can we color that node?
冈崎算法。更有趣的是 insert 操作。与标准二叉树一样,我们通过替换搜索过程找到的叶子来添加节点。但是我们可以给这个节点上什么颜色呢?

  • Coloring it black could increase the black height of that path, violating the Global Invariant.
    将其涂成黑色可能会增加该路径的黑色高度,从而违反全局不变式。

  • Coloring it red could make it adjacent to another red node, violating the Local Invariant.
    将其着色为红色可能会使其与另一个红色节点相邻,从而违反局部不变式。

So neither choice is safe in general. Chris Okasaki (Purely Functional Data Structures, 1999) gives an elegant algorithm that solves the problem by opting to violate the Local Invariant, then walk up the tree to repair the violation. Here’s how it works.
所以总的来说,这两种选择都不安全。 Chris Okasaki(纯函数式数据结构,1999)给出了一种优雅的算法,该算法通过选择违反局部不变量来解决问题,然后沿着树向上修复违规行为。这是它的工作原理。

We always color the new node red to ensure that the Global Invariant is preserved. However, this may destroy the Local Invariant by producing two adjacent red nodes. In order to restore the invariant, we consider not only the new red node and its red parent, but also its (black) grandparent.
我们总是将新节点着色为红色以确保保留全局不变式。然而,这可能会通过产生两个相邻的红色节点来破坏局部不变量。为了恢复不变量,我们不仅考虑新的红色节点及其红色父节点,还考虑其(黑色)祖父节点。

The next figure shows the four possible cases that can arise. In it, a-d are possibly empty subtrees, and x-z are values stored at a node. The nodes colors are indicated with R and B.
下图显示了可能出现的四种可能情况。其中, a - d 可能是空子树, x - z 是存储在节点处的值。节点颜色用 RB 指示。

           1             2             3             4

           Bz            Bz            Bx            Bx
          / \           / \           / \           / \
         Ry  d         Rx  d         a   Rz        a   Ry
        /  \          / \               /  \          /  \
      Rx   c         a   Ry            Ry   d        b    Rz
     /  \               /  \          / \                /  \
    a    b             b    c        b   c              c    d

Notice that in each of these trees, we’ve carefully labeled the values and nodes such that the binary search tree invariant ensures the following ordering:
请注意,在每棵树中,我们都仔细标记了值和节点,以便二叉搜索树不变式确保以下排序:

all nodes in a
 <
  x
   <
    all nodes in b
     <
      y
       <
        all nodes in c
         <
          z
           <
            all nodes in d

Therefore, we can transform the tree to restore the invariant locally by replacing any of the above four cases with:
因此,我们可以通过将上述四种情况中的任何一种替换为:

         Ry
        /  \
      Bx    Bz
     / \   / \
    a   b c   d

Tip

To really understand Okasaki’s algorithm, ensure that the last three diagrams make sense. The choice of which labels are placed where in the first diagram is crucial. That’s what guarantees the ordering holds, hence that the final tree is the same in all four cases.
要真正理解冈崎的算法,请确保最后三张图有意义。选择将哪些标签放置在第一个图中的位置至关重要。这就是保证顺序不变的原因,因此最终的树在所有四种情况下都是相同的。

This balance function can be written simply and concisely using pattern matching, where each of the four input cases is mapped to the same output case. In addition, there is the case where the tree is left unchanged locally.
这个平衡函数可以使用模式匹配来简单而简洁地编写,其中四个输入情况中的每一个都映射到相同的输出情况。另外,还存在树在本地保持不变的情况。

let balance = function
  | Black, z, Node (Red, y, Node (Red, x, a, b), c), d
  | Black, z, Node (Red, x, a, Node (Red, y, b, c)), d
  | Black, x, a, Node (Red, z, Node (Red, y, b, c), d)
  | Black, x, a, Node (Red, y, b, Node (Red, z, c, d)) ->
    Node (Red, y, Node (Black, x, a, b), Node (Black, z, c, d))
  | a, b, c, d -> Node (a, b, c, d)
val balance : color * 'a * 'a rbtree * 'a rbtree -> 'a rbtree = <fun>

This balancing transformation possibly breaks the Local Invariant one level up in the tree, but it can be restored again at that level in the same way, and so on up the tree. In the worst case, the process cascades all the way up to the root, resulting in two adjacent red nodes, one of them the root. But if this happens, we can just recolor the root black, which increases the black height by one. The amount of work is O(logn). The insert code using balance is as follows:
这种平衡变换可能会破坏树中上一层的局部不变量,但它可以在该层以相同的方式再次恢复,依此类推。在最坏的情况下,该过程一路级联到根,产生两个相邻的红色节点,其中之一是根。但如果发生这种情况,我们可以将根重新着色为黑色,这会将黑色高度增加一。工作量为 O(logn) 。使用 balanceinsert 代码如下:

let insert x s =
  let rec ins = function
    | Leaf -> Node (Red, x, Leaf, Leaf)
    | Node (color, y, a, b) as s ->
      if x < y then balance (color, y, ins a, b)
      else if x > y then balance (color, y, a, ins b)
      else s
  in
  match ins s with
  | Node (_, y, a, b) -> Node (Black, y, a, b)
  | Leaf -> (* guaranteed to be nonempty *)
    failwith "RBT insert failed with ins returning leaf"
val insert : 'a -> 'a rbtree -> 'a rbtree = <fun>

The remove operation. Removing an element from a red-black tree works analogously. We start with a BST element removal and then do rebalancing. When an interior (nonleaf) node is removed, we simply splice it out if it has fewer than two nonleaf children; if it has two nonleaf children, we find the next value in the tree, which must be found inside its right child.
删除运算。从红黑树中删除元素的工作方式类似。我们从删除 BST 元素开始,然后进行重新平衡。当删除内部(非叶)节点时,如果它的非叶子节点少于两个,我们只需将其拼接即可;如果它有两个非叶子节点,我们将在树中查找下一个值,该值必须在其右子节点中找到。

But, balancing the trees during removal from red-black tree requires considering more cases. Deleting a black element from the tree creates the possibility that some path in the tree has too few black nodes, breaking the Global Invariant.
但是,在从红黑树中删除过程中平衡树需要考虑更多的情况。从树中删除黑色元素可能会导致树中某些路径的黑色节点太少,从而破坏全局不变式。

Germane and Might invented an elegant algorithm to handle that rebalancing Their solution is to create “doubly-black” nodes that count twice in determining the black height. For more, read their paper: [Deletion: The Curse of the Red-Black Tree Journal of Functional Programming], volume 24, issue 4, July 2014.
Germane 和 Might 发明了一种优雅的算法来处理这种重新平衡,他们的解决方案是创建“双黑”节点,在确定黑色高度时计算两次。有关更多信息,请阅读他们的论文:《删除:函数式编程红黑树期刊的诅咒》,第 24 卷,第 4 期,2014 年 7 月。

8.3.3. Maps and Sets from BSTs
8.3.3. 来自 BST 的映射和集合 ¶

It’s easy to use a BST to implement either a map or a set ADT:
使用 BST 来实现映射或集合 ADT 很容易:

  • For a map, just store a binding at each node. The nodes are ordered by the keys. The values are irrelevant to the ordering.
    对于映射,只需在每个节点存储一个绑定即可。节点按键排序。这些值与顺序无关。

  • For a set, just store an element at each node. The nodes are ordered by the elements.
    对于集合,只需在每个节点存储一个元素即可。节点按元素排序。

The OCaml standard library does this for the Map and Set modules. It uses a balanced BST that is a variant of an AVL tree. AVL trees are balanced BSTs in which the height of paths is allowed to vary by at most 1. The OCaml standard library modifies that to allow the height to vary by at most 2. Like red-black trees, they achieve worst-case logarithmic performance.
OCaml 标准库对 MapSet 模块执行此操作。它使用平衡 BST,它是 AVL 树的变体。 AVL 树是平衡 BST,其中路径的高度最多允许变化 1。OCaml 标准库对此进行了修改,以允许高度最多变化 2。与红黑树一样,它们实现了最坏情况对数性能。

Now that we have a functional map data structure, how does it compare to our imperative version, the hash table?
现在我们有了一个函数式映射数据结构,它与我们的命令式版本(哈希表)相比如何?

  • Persistence: Our red-black trees are persistent, but hash tables are ephemeral.
    持久性:我们的红黑树是持久的,但哈希表是短暂的。

  • Performance: We get guaranteed worst-case logarithmic performance with red-black trees, but amortized, expected constant-time with hash tables. That’s somewhat hard to compare given all the modifiers involved. It’s also an example of a general phenomenon that persistent data structures often have to pay an extra logarithmic cost over the equivalent ephemeral data structures.
    性能:我们通过红黑树获得保证的最坏情况对数性能,但通过哈希表获得摊销的预期常数时间。考虑到涉及的所有修饰符,这有点难以比较。这也是一个普遍现象的例子,即持久数据结构通常必须比等效的临时数据结构付出额外的对数成本。

  • Convenience: We have to provide an ordering function for balanced binary trees, and a hash function for hash tables. Most libraries provide a default hash function for convenience. But the performance of the hash table does depend on that hash function truly distributing keys randomly over buckets. If it doesn’t, the “expected” part of the performance guarantee for hash tables is violated. So the convenience is a double-edged sword.
    便利性:我们必须为平衡二叉树提供排序函数,为哈希表提供哈希函数。为了方便起见,大多数库都提供默认的哈希函数。但哈希表的性能确实取决于哈希函数是否真正将键随机分布在存储桶上。如果不是,则违反了哈希表性能保证的“预期”部分。所以便利性是一把双刃剑。

There isn’t a clear winner here. Since the OCaml library provides both Map and Hashtbl, you get to choose.
这里没有明显的赢家。由于 OCaml 库同时提供 MapHashtbl ,您可以进行选择。

8.4. Sequences 8.4. 序列 ¶

A sequence is an infinite list. For example, the infinite list of all natural numbers would be a sequence. So would the list of all primes, or all Fibonacci numbers. How can we efficiently represent infinite lists? Obviously we can’t store the whole list in memory.
序列是一个无限列表。例如,所有自然数的无限列表将是一个序列。所有素数或所有斐波那契数的列表也是如此。我们如何有效地表示无限列表?显然我们不能将整个列表存储在内存中。

We already know that OCaml allows us to create recursive functions—that is, functions defined in terms of themselves. It turns out we can define other values in terms of themselves, too.
我们已经知道 OCaml 允许我们创建递归函数,即根据自身定义的函数。事实证明,我们也可以根据其他价值观本身来定义它们。

let rec ones = 1 :: ones
val ones : int list = [1; <cycle>]
let rec a = 0 :: b and b = 1 :: a
val a : int list = [0; 1; <cycle>]
val b : int list = [1; 0; <cycle>]

The expressions above create recursive values. The list ones contains an infinite sequence of 1, and the lists a and b alternate infinitely between 0 and 1. As the lists are infinite, the toplevel cannot print them in their entirety. Instead, it indicates a cycle: the list cycles back to its beginning. Even though these lists represent an infinite sequence of values, their representation in memory is finite: they are linked lists with back pointers that create those cycles.
上面的表达式创建递归值。列表 ones 包含无限序列 1 ,列表 ab0 之间无限交替和 1 。由于列表是无限的,顶层无法完整打印它们。相反,它表示一个循环:列表循环回到其开头。尽管这些列表表示无限的值序列,但它们在内存中的表示是有限的:它们是带有创建这些循环的反向指针的链表。

Beyond sequences of numbers, there are other kinds of infinite mathematical objects we might want to represent with finite data structures:
除了数字序列之外,我们可能还想用有限数据结构来表示其他类型的无限数学对象:

  • A stream of inputs read from a file, a network socket, or a user. All of these are unbounded in length, hence we can think of them as being infinite in length. In fact, many I/O libraries treat reaching the end of an I/O stream as an unexpected situation and raise an exception.
    从文件、网络套接字或用户读取的输入流。所有这些的长度都是无限的,因此我们可以认为它们的长度是无限的。事实上,许多 I/O 库将到达 I/O 流末尾视为意外情况并引发异常。

  • A game tree is a tree in which the positions of a game (e.g., chess or tic-tac-toe)_ are the nodes and the edges are possible moves. For some games this tree is in fact infinite (imagine, e.g., that the pieces on the board could chase each other around forever), and for other games, it’s so deep that we would never want to manifest the entire tree, hence it is effectively infinite.
    游戏树是一种特殊的树,其中游戏的位置(例如,国际象棋或井字棋)是节点,边是可能的移动。对于某些游戏来说,这棵树实际上是无限的(例如,想象一下,棋盘上的棋子可以永远互相追逐),而对于其他游戏来说,它是如此之深,以至于我们永远不想显现整棵树,因此它是实际上是无限的。

8.4.1. How Not to Define a Sequence
8.4.1. 如何不定义序列 ¶

Suppose we wanted to represent the first of those examples: the sequence of all natural numbers. Some of the obvious things we might try simply don’t work:
假设我们想要表示第一个例子:所有自然数的序列。我们可能尝试的一些显而易见的事情根本行不通:

(** [from n] is the infinite list [[n; n + 1; n + 2; ...]]. *)
let rec from n = n :: from (n + 1)
val from : int -> int list = <fun>
(** [nats] is the infinite list of natural numbers [[0; 1; ...]]. *)
let nats = from 0
Stack overflow during evaluation (looping recursion?).

The problem with that attempt is that nats attempts to compute the entire infinite sequence of natural numbers. Because the function isn’t tail recursive, it quickly overflows the stack. If it were tail recursive, it would go into an infinite loop.
该尝试的问题在于 nats 尝试计算自然数的整个无限序列。由于该函数不是尾递归,因此它很快就会溢出堆栈。如果是尾递归,就会进入无限循环。

Here’s another attempt, using what we discovered above about recursive values:
这是另一种尝试,使用我们上面发现的有关递归值的内容:

let rec nats = 0 :: List.map (fun x -> x + 1) nats
File "[4]", line 1, characters 15-50:
1 | let rec nats = 0 :: List.map (fun x -> x + 1) nats
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This kind of expression is not allowed as right-hand side of `let rec'

That attempt doesn’t work for a more subtle reason. In the definition of a recursive value, we are not permitted to use a value before it is finished being defined. The problem is that List.map is applied to nats, and therefore pattern matches to extract the head and tail of nats. But we are in the middle of defining nats, so that use of nats is not permitted.
由于更微妙的原因,这种尝试不起作用。在递归值的定义中,我们不允许在定义完成之前使用一个值。问题在于 List.map 应用于 nats ,因此模式匹配以提取 nats 的头部和尾部。但我们正在定义 nats ,因此不允许使用 nats

8.4.2. How to Correctly Define a Sequence
8.4.2. 如何正确定义序列 ¶

We can try to define a sequence by analogy to how we can define (finite) lists. Recall that definition:
我们可以尝试通过类比定义(有限)列表来定义序列。回想一下这个定义:

type 'a mylist = Nil | Cons of 'a * 'a mylist
type 'a mylist = Nil | Cons of 'a * 'a mylist

We could try to convert that into a definition for sequences:
我们可以尝试将其转换为序列的定义:

type 'a sequence = Cons of 'a * 'a sequence
type 'a sequence = Cons of 'a * 'a sequence

Note that we got rid of the Nil constructor, because the empty list is finite, but we want only infinite lists.
请注意,我们删除了 Nil 构造函数,因为空列表是有限的,但我们只需要无限列表。

The problem with that definition is that it’s really no better than the built-in list in OCaml, in that we still can’t define nats:
该定义的问题在于它实际上并不比 OCaml 中的内置列表更好,因为我们仍然无法定义 nats

let rec from n = Cons (n, from (n + 1))
val from : int -> int sequence = <fun>
let nats = from 0
Stack overflow during evaluation (looping recursion?).

As before, that definition attempts to go off and compute the entire infinite sequence of naturals.
和以前一样,该定义试图开始计算整个自然数的无限序列。

What we need is a way to pause evaluation, so that at any point in time, only a finite approximation to the infinite sequence has been computed. Fortunately, we already know how to do that!
我们需要的是一种暂停计算的方法,以便在任何时间点,只计算无限序列的有限近似。幸运的是,我们已经知道如何做到这一点!

Consider the following definitions:
考虑以下定义:

let f1 = failwith "oops"
Exception: Failure "oops".
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from unknown location
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150
let f2 = fun x -> failwith "oops"
val f2 : 'a -> 'b = <fun>
f2 ();;
Exception: Failure "oops".
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

The definition of f1 immediately raises an exception, whereas the definition of f2 does not. Why? Because f2 wraps the failwith inside an anonymous function. Recall that, according to the dynamic semantics of OCaml, functions are already values. So no computation is done inside the body of the function until it is applied. That’s why f2 () raises an exception.
f1 的定义立即引发异常,而 f2 的定义则不会。为什么?因为 f2failwith 包装在匿名函数内。回想一下,根据 OCaml 的动态语义,函数已经是值。因此,在应用函数之前,不会在函数体内进行任何计算。这就是 f2 () 引发异常的原因。

We can use this property of evaluation—that functions delay evaluation—to our advantage in defining sequences: let’s wrap the tail of a sequence inside a function. Since it doesn’t really matter what argument that function takes, we might as well let it be unit. A function that is used just to delay computation, and in particular one that takes unit as input, is called a thunk.
我们可以利用求值的这个属性(函数延迟求值)来定义序列:让我们将序列的尾部包装在函数内。由于该函数采用什么参数并不重要,因此我们不妨将其设为单位。仅用于延迟计算的函数,特别是采用单位作为输入的函数,称为 thunk

(** An ['a sequence] is an infinite list of values of type ['a].
    AF: [Cons (x, f)] is the sequence whose head is [x] and tail is [f ()].
    RI: none. *)
type 'a sequence = Cons of 'a * (unit -> 'a sequence)
type 'a sequence = Cons of 'a * (unit -> 'a sequence)

This definition turns out to work quite well. We can define nats, at last:
事实证明这个定义非常有效。最后我们可以定义 nats

let rec from n = Cons (n, fun () -> from (n + 1))
let nats = from 0
val from : int -> int sequence = <fun>
val nats : int sequence = Cons (0, <fun>)

We do not get an infinite loop or a stack overflow. The evaluation of nats has paused. Only the first element of it, 0, has been computed. The remaining elements will not be computed until they are requested. To do that, we can define functions to access parts of a sequence, similarly to how we can access parts of a list:
我们不会遇到无限循环或堆栈溢出。 nats 的评估已暂停。仅计算了它的第一个元素 0 。剩余元素在被请求之前不会被计算。为此,我们可以定义函数来访问序列的部分,类似于访问列表的部分:

(** [hd s] is the head of [s] *)
let hd (Cons (h, _)) = h
val hd : 'a sequence -> 'a = <fun>
(** [tl s] is the tail of [s] *)
let tl (Cons (_, t)) = t ()
val tl : 'a sequence -> 'a sequence = <fun>

Note how, in the definition of tl, we must apply the function t to () to obtain the tail of the sequence. That is, we must force the thunk to evaluate at that point, rather than continue to delay its computation.
请注意,在 tl 的定义中,我们必须将函数 t 应用于 () 来获取序列的尾部。也就是说,我们必须强制 thunk 在该点进行计算,而不是继续延迟其计算。

For convenience, we can write functions that apply hd or tl multiple times to take or drop some finite prefix of a sequence:
为了方便起见,我们可以编写多次应用 hdtl 的函数来获取或删除序列的某些有限前缀:

(** [take n s] is the list of the first [n] elements of [s] *)
let rec take n s =
  if n = 0 then [] else hd s :: take (n - 1) (tl s)

(** [drop n s] is all but the first [n] elements of [s] *)
let rec drop n s =
  if n = 0 then s else drop (n - 1) (tl s)
val take : int -> 'a sequence -> 'a list = <fun>
val drop : int -> 'a sequence -> 'a sequence = <fun>

For example: 例如:

take 10 nats
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

8.4.3. Programming with Sequences
8.4.3. 使用序列编程 ¶

Let’s write some functions that manipulate sequences. It will help to have a notation for sequences to use as part of documentation. Let’s use <a; b; c; ...> to denote the sequence that has elements a, b, and c at its head, followed by infinitely many other elements.
让我们编写一些操作序列的函数。将序列符号用作文档的一部分将有所帮助。让我们使用 <a; b; c; ...> 来表示在其头部具有元素 abc 元素的序列,后跟无限多个其他元素。

Here are functions to square a sequence, and to sum two sequences:
以下是对序列求平方以及对两个序列求和的函数:

(** [square <a; b; c; ...>] is [<a * a; b * b; c * c; ...]. *)
let rec square (Cons (h, t)) =
  Cons (h * h, fun () -> square (t ()))

(** [sum <a1; a2; a3; ...> <b1; b2; b3; ...>] is
    [<a1 + b1; a2 + b2; a3 + b3; ...>] *)
let rec sum (Cons (h1, t1)) (Cons (h2, t2)) =
  Cons (h1 + h2, fun () -> sum (t1 ()) (t2 ()))
val square : int sequence -> int sequence = <fun>
val sum : int sequence -> int sequence -> int sequence = <fun>

Note how the basic template for defining both functions is the same:
请注意定义这两个函数的基本模板是如何相同的:

  • Pattern match against the input sequence(s), which must be Cons of a head and a tail function (a thunk).
    与输入序列进行模式匹配,输入序列必须是 head 和 tail 函数(thunk)的 Cons

  • Construct a sequence as the output, which must be Cons of a new head and a new tail function (a thunk).
    构造一个序列作为输出,它必须是一个新的 head 和一个新的 tail 函数(一个 thunk)的 Cons

  • In constructing the new tail function, delay the evaluation of the tail by immediately starting with fun () -> ....
    在构造新的 tail 函数时,通过立即从 fun () -> ... 开始来延迟对 tail 的评估。

  • Inside the body of that thunk, recursively apply the function being defined (square or sum) to the result of forcing a thunk (or thunks) to evaluate.
    在该 thunk 的主体内,递归地将定义的函数(平方或和)应用于强制 thunk(或多个 thunk)求值的结果。

Of course, squaring and summing are just two possible ways of mapping a function across a sequence or sequences. That suggests we could write a higher-order map function, much like for lists:
当然,平方和求和只是将函数映射到一个或多个序列的两种可能的方式。这表明我们可以编写一个高阶映射函数,就像列表一样:

(** [map f <a; b; c; ...>] is [<f a; f b; f c; ...>] *)
let rec map f (Cons (h, t)) =
  Cons (f h, fun () -> map f (t ()))

(** [map2 f <a1; b1; c1;...> <a2; b2; c2; ...>] is
    [<f a1 b1; f a2 b2; f a3 b3; ...>] *)
let rec map2 f (Cons (h1, t1)) (Cons (h2, t2)) =
  Cons (f h1 h2, fun () -> map2 f (t1 ()) (t2 ()))

let square' = map (fun n -> n * n)
let sum' = map2 ( + )
val map : ('a -> 'b) -> 'a sequence -> 'b sequence = <fun>
val map2 : ('a -> 'b -> 'c) -> 'a sequence -> 'b sequence -> 'c sequence =
  <fun>
val square' : int sequence -> int sequence = <fun>
val sum' : int sequence -> int sequence -> int sequence = <fun>

Now that we have a map function for sequences, we can successfully define nats in one of the clever ways we originally attempted:
现在我们有了序列的映射函数,我们可以用我们最初尝试的巧妙方法之一成功定义 nats

let rec nats = Cons (0, fun () -> map (fun x -> x + 1) nats)
val nats : int sequence = Cons (0, <fun>)
take 10 nats
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

Why does this work? Intuitively, nats is <0; 1; 2; 3; ...>, so mapping the increment function over nats is <1; 2; 3; 4; ...>. If we cons 0 onto the beginning of <1; 2; 3; 4; ...>, we get <0; 1; 2; 3; ...>, as desired. The recursive value definition is permitted, because we never attempt to use nats until after its definition is finished. In particular, the thunk delays nats from being evaluated on the right-hand side of the definition.
为什么这有效?直观上, nats<0; 1; 2; 3; ...> ,因此将增量函数映射到 nats 上是 <1; 2; 3; 4; ...> 。如果我们将 0 放到 <1; 2; 3; 4; ...> 的开头,我们就会根据需要得到 <0; 1; 2; 3; ...> 。递归值定义是允许的,因为在定义完成之前我们不会尝试使用 nats 。特别是,thunk 会延迟 nats 在定义的右侧进行计算。

Here’s another clever definition. Consider the Fibonacci sequence <1; 1; 2; 3; 5; 8; ...>. If we take the tail of it, we get <1; 2; 3; 5; 8; 13; ...>. If we sum those two sequences, we get <2; 3; 5; 8; 13; 21; ...>. That’s nothing other than the tail of the tail of the Fibonacci sequence. So if we were to prepend [1; 1] to it, we’d have the actual Fibonacci sequence. That’s the intuition behind this definition:
这是另一个巧妙的定义。考虑斐波那契数列 <1; 1; 2; 3; 5; 8; ...> 。如果我们取它的尾部,我们会得到 <1; 2; 3; 5; 8; 13; ...> 。如果我们将这两个序列相加,我们就得到 <2; 3; 5; 8; 13; 21; ...> 。这只不过是斐波那契数列尾部的尾部。因此,如果我们在其前面添加 [1; 1] ,我们就会得到实际的斐波那契数列。这就是这个定义背后的直觉:

let rec fibs =
  Cons (1, fun () ->
    Cons (1, fun () ->
      sum fibs (tl fibs)))
val fibs : int sequence = Cons (1, <fun>)

And it works! 它有效!

take 10 fibs
- : int list = [1; 1; 2; 3; 5; 8; 13; 21; 34; 55]

Unfortunately, it’s highly inefficient. Every time we force the computation of the next element, it required recomputing all the previous elements, twice: once for fibs and once for tl fibs in the last line of the definition. Try running the code yourself. By the time we get up to the 30th number, the computation is noticeably slow; by the time of the 100th, it seems to last forever.
不幸的是,它的效率非常低。每次我们强制计算下一个元素时,都需要重新计算所有先前的元素,两次:一次用于定义最后一行的 fibs ,一次用于 tl fibs 。尝试自己运行代码。当我们到达第 30 个数字时,计算速度明显变慢;到了第100次的时候,它似乎会永远持续下去。

Could we do better? Yes, with a little help from a new language feature: laziness. We discuss it, next.
我们可以做得更好吗?是的,需要一个新的语言特性的一点帮助:惰性。我们接下来讨论一下。

8.4.4. Laziness
8.4.4. 惰性 ¶

The example with the Fibonacci sequence demonstrates that it would be useful if the computation of a thunk happened only once: when it is forced, the resulting value could be remembered, and if the thunk is ever forced again, that value could immediately be returned instead of recomputing it. That’s the idea behind the OCaml Lazy module:
斐波那契数列的示例表明,如果 thunk 的计算仅发生一次,那么将会很有用:当强制执行该操作时,可以记住结果值,并且如果再次强制执行 thunk,则可以立即返回该值重新计算它。这就是 OCaml Lazy 模块背后的想法:

module Lazy :
  sig
    type 'a t = 'a lazy_t
    val force : 'a t -> 'a
    ...
  end

A value of type 'a Lazy.t is a value of type 'a whose computation has been delayed. Intuitively, the language is being lazy about evaluating it: it won’t be computed until specifically demanded. The way that demand is expressed with by forcing the evaluation with Lazy.force, which takes the 'a Lazy.t and causes the 'a inside it to finally be produced. The first time a lazy value is forced, the computation might take a long time. But the result is cached aka memoized, and any subsequent time that lazy value is forced, the memoized result will be returned immediately without recomputing it.
'a Lazy.t 类型的值是计算已延迟的 'a 类型的值。直观上,该语言懒于评估它:除非有特别要求,否则不会计算它。通过强制使用 Lazy.force 进行评估来表达需求的方式,它采用 'a Lazy.t 并导致最终生成其中的 'a 。第一次强制使用惰性值时,计算可能需要很长时间。但结果会被缓存(也称为记忆),并且以后任何时候强制使用惰性值时,记忆结果都将立即返回,而无需重新计算。

Note 笔记

“Memoized” really is the correct spelling of this term. We didn’t misspell “memorized”, though it might look that way.
“Memoized”确实是这个词的正确拼写。我们没有拼错“memorized”,尽管看起来可能是这样。

The Lazy module doesn’t contain a function that produces a 'a Lazy.t. Instead, there is a keyword built-in to the OCaml syntax that does it: lazy e.
Lazy 模块不包含生成 'a Lazy.t 的函数。相反,OCaml 语法中有一个内置关键字可以执行此操作: lazy e

  • Syntax: lazy e 语法: lazy e

  • Static semantics: If e : u, then lazy e : u Lazy.t.
    静态语义:如果 e : u ,则 lazy e : u Lazy.t

  • Dynamic semantics: lazy e does not evaluate e to a value. Instead it produces a suspension that, when later forced, will evaluate e to a value v and return v. Moreover, that suspension remembers that v is its forced value. And if the suspension is ever forced again, it immediately returns v instead of recomputing it.
    动态语义: lazy e 不会将 e 计算为值。相反,它会产生一个暂停,当稍后强制时,会将 e 计算为值 v 并返回 v 。此外,该暂停会记住 v 是其强制值。如果再次强制暂停,它会立即返回 v 而不是重新计算。

Note 笔记

OCaml’s usual evaluation strategy is eager aka strict: it always evaluate an argument before function application. If you want a value to be computed lazily, you must specifically request that with the lazy keyword. Other function languages, notably Haskell, are lazy by default. Laziness can be pleasant when programming with infinite data structures. But lazy evaluation makes it harder to reason about space and time, and it has unpleasant interactions with side effects.
OCaml 通常的评估策略是 eager 又名 strict:它总是在函数应用之前评估参数。如果您希望延迟计算值,则必须使用 lazy 关键字明确请求。其他函数语言,尤其是 Haskell,默认情况下是惰性的。使用无限数据结构进行编程时,懒惰可能是令人愉快的。但惰性求值使得推理空间和时间变得更加困难,并且它与副作用之间存在令人不快的相互作用。

To illustrate the use of lazy values, let’s try computing the 30th Fibonacci number using this definition of fibs:
为了说明惰性值的使用,让我们尝试使用 fibs 的定义来计算第 30 个斐波那契数:

let rec fibs =
  Cons (1, fun () ->
    Cons (1, fun () ->
      sum fibs (tl fibs)))
val fibs : int sequence = Cons (1, <fun>)

Tip

These next few examples will make much more sense if you run them interactively, rather than just reading this page.
如果您以交互方式运行接下来的几个示例,而不仅仅是阅读本页,那么它们将更有意义。

If we try to get the 30th Fibonacci number, it will take a long time to compute:
如果我们尝试获取第 30 个斐波那契数,则需要很长时间来计算:

let fib30long = take 30 fibs |> List.rev |> List.hd
val fib30long : int = 832040

But if we wrap evaluation of that with lazy, it will return immediately, because the evaluation of that number has been suspended:
但是如果我们用 lazy 包装对该数字的评估,它将立即返回,因为对该数字的评估已暂停:

let fib30lazy = lazy (take 30 fibs |> List.rev |> List.hd)
val fib30lazy : int lazy_t = <lazy>

Later on we could force the evaluation of that lazy value, and that will take a long time to compute, as did fib30long:
稍后我们可以强制计算该惰性值,这将需要很长时间来计算,就像 fib30long 一样:

let fib30 = Lazy.force fib30lazy
val fib30 : int = 832040

But if we ever try to recompute that same lazy value, it will return immediately, because the result has been memoized:
但是,如果我们尝试重新计算相同的惰性值,它将立即返回,因为结果已被记忆:

let fib30fast = Lazy.force fib30lazy
val fib30fast : int = 832040

Nonetheless, we still haven’t totally succeeded. That particular computation of the 30th Fibonacci number has been memoized, but if we later define some other computation of another it won’t be sped up the first time it’s computed:
尽管如此,我们仍然没有完全成功。第 30 个斐波那契数的特定计算已被记忆,但如果我们稍后定义另一个数的其他计算,则在第一次计算时不会加速:

let fib29 = take 29 fibs |> List.rev |> List.hd
val fib29 : int = 514229

What we really want is to change the representation of sequences itself to make use of lazy values.
我们真正想要的是改变序列本身的表示以利用惰性值。

8.4.4.1. Lazy Sequences
8.4.4.1. 惰性序列 ¶

Here’s a representation for infinite lists using lazy values:
这是使用惰性值的无限列表的表示:

type 'a lazysequence = Cons of 'a * 'a lazysequence Lazy.t
type 'a lazysequence = Cons of 'a * 'a lazysequence Lazy.t

We’ve gotten rid of the thunk, and instead are using a lazy value as the tail of the lazy sequence. If we ever want that tail to be computed, we force it.
我们已经摆脱了 thunk,而是使用惰性值作为惰性序列的尾部。如果我们想要计算尾部,我们会强制它。

For sake of comparison, the following two modules implement the Fibonacci sequence with sequences, then with lazy sequences. Try computing the 30th Fibonacci number with both modules, and you’ll see that the lazy-sequence implementation is much faster than the standard-sequence implementation.
为了进行比较,以下两个模块使用序列实现斐波那契序列,然后使用惰性序列实现斐波那契序列。尝试使用两个模块计算第 30 个斐波那契数,您会发现惰性序列实现比标准序列实现快得多。

module SequenceFibs = struct
  type 'a sequence = Cons of 'a * (unit -> 'a sequence)

  let hd : 'a sequence -> 'a =
    fun (Cons (h, _)) -> h

  let tl : 'a sequence -> 'a sequence =
    fun (Cons (_, t)) -> t ()

  let rec take_aux n (Cons (h, t)) lst =
    if n = 0 then lst
    else take_aux (n - 1) (t ()) (h :: lst)

  let take : int -> 'a sequence -> 'a list =
    fun n s -> List.rev (take_aux n s [])

  let nth : int -> 'a sequence -> 'a =
    fun n s -> List.hd (take_aux (n + 1) s [])

  let rec sum : int sequence -> int sequence -> int sequence =
    fun (Cons (h_a, t_a)) (Cons (h_b, t_b)) ->
      Cons (h_a + h_b, fun () -> sum (t_a ()) (t_b ()))

  let rec fibs =
    Cons(1, fun () ->
      Cons(1, fun () ->
        sum (tl fibs) fibs))

  let nth_fib n =
    nth n fibs

end

module LazyFibs = struct

  type 'a lazysequence = Cons of 'a * 'a lazysequence Lazy.t

  let hd : 'a lazysequence -> 'a =
    fun (Cons (h, _)) -> h

  let tl : 'a lazysequence -> 'a lazysequence =
    fun (Cons (_, t)) -> Lazy.force t

  let rec take_aux n (Cons (h, t)) lst =
    if n = 0 then lst else
      take_aux (n - 1) (Lazy.force t) (h :: lst)

  let take : int -> 'a lazysequence -> 'a list =
    fun n s -> List.rev (take_aux n s [])

  let nth : int -> 'a lazysequence -> 'a =
    fun n s -> List.hd (take_aux (n + 1) s [])

  let rec sum : int lazysequence -> int lazysequence -> int lazysequence =
    fun (Cons (h_a, t_a)) (Cons (h_b, t_b)) ->
      Cons (h_a + h_b, lazy (sum (Lazy.force t_a) (Lazy.force t_b)))

  let rec fibs =
    Cons(1, lazy (
      Cons(1, lazy (
        sum (tl fibs) fibs))))

  let nth_fib n =
    nth n fibs
end
module SequenceFibs :
  sig
    type 'a sequence = Cons of 'a * (unit -> 'a sequence)
    val hd : 'a sequence -> 'a
    val tl : 'a sequence -> 'a sequence
    val take_aux : int -> 'a sequence -> 'a list -> 'a list
    val take : int -> 'a sequence -> 'a list
    val nth : int -> 'a sequence -> 'a
    val sum : int sequence -> int sequence -> int sequence
    val fibs : int sequence
    val nth_fib : int -> int
  end
module LazyFibs :
  sig
    type 'a lazysequence = Cons of 'a * 'a lazysequence Lazy.t
    val hd : 'a lazysequence -> 'a
    val tl : 'a lazysequence -> 'a lazysequence
    val take_aux : int -> 'a lazysequence -> 'a list -> 'a list
    val take : int -> 'a lazysequence -> 'a list
    val nth : int -> 'a lazysequence -> 'a
    val sum : int lazysequence -> int lazysequence -> int lazysequence
    val fibs : int lazysequence
    val nth_fib : int -> int
  end

8.5. Memoization 8.5. 记忆化 ¶

In the previous section, we saw that the Lazy module memoizes the results of computations, so that no time has to be wasted on recomputing them. Memoization is a powerful technique for asymptotically speeding up simple recursive algorithms, without having to change the way the algorithm works.
在上一节中,我们看到 Lazy 模块会记住计算结果,因此不必浪费时间重新计算它们。记忆化是一种强大的技术,可以渐近加速简单的递归算法,而无需改变算法的工作方式。

Let’s see apply the Abstraction Principle and invent a way to memoize any function, so that the function only had to be evaluated once on any given input. We’ll end up using imperative data structures (arrays and hash tables) as part of our solution.
让我们看看应用抽象原则并发明一种方法来记忆任何函数,以便该函数只需在任何给定输入上计算一次。我们最终将使用命令式数据结构(数组和哈希表)作为我们解决方案的一部分。

8.5.1. Fibonacci 8.5.1. 斐波那契 ¶

Let’s again consider the problem of computing the nth Fibonacci number. The naive recursive implementation takes exponential time, because of the recomputation of the same Fibonacci numbers over and over again:
让我们再次考虑计算第 n 个斐波那契数的问题。简单的递归实现需要指数时间,因为一遍又一遍地重新计算相同的斐波那契数:

let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2)
val fib : int -> int = <fun>

Note 笔记

To be precise, its running time turns out to be O(ϕn), where ϕ is the golden ratio, 1+52.
准确地说,它的运行时间是 O(ϕn) ,其中 ϕ 是黄金比例, 1+52

If we record Fibonacci numbers as they are computed, we can avoid this redundant work. The idea is that whenever we compute f n, we store it in a table indexed by n. In this case the indexing keys are integers, so we can use implement this table using an array:
如果我们在计算斐波那契数时记录它们,我们就可以避免这种多余的工作。这个想法是,每当我们计算 f n 时,我们都会将其存储在由 n 索引的表中。在这种情况下,索引键是整数,因此我们可以使用数组来实现此表:

let fibm n =
  let memo : int option array = Array.make (n + 1) None in
  let rec f_mem n =
    match memo.(n) with
    | Some result -> (* computed already *) result
    | None ->
        let result =
          if n < 2 then 1 else f_mem (n - 1) + f_mem (n - 2)
        in
        (* record in table *)
        memo.(n) <- Some result;
        result
  in
  f_mem n
val fibm : int -> int = <fun>

The function f_mem defined inside fibm contains the original recursive algorithm, except before doing that calculation it first checks if the result has already been computed and stored in the table in which case it simply returns the result.
fibm 内定义的函数 f_mem 包含原始递归算法,但在进行计算之前,它首先检查结果是否已计算并存储在表中,在这种情况下,它只是返回结果。

How do we analyze the running time of this function? The time spent in a single call to f_mem is O(1) if we exclude the time spent in any recursive calls that it happens to make. Now we look for a way to bound the total number of recursive calls by finding some measure of the progress that is being made.
我们如何分析这个函数的运行时间呢?如果我们排除发生的任何递归调用所花费的时间,则单次调用 f_mem 所花费的时间为 O(1) 。现在,我们通过找到某种进度度量来寻找一种方法来限制递归调用的总数。

A good choice of progress measure, not only here but also for many uses of memoization, is the number of nonempty entries in the table (i.e. entries that contain Some n rather than None). Each time f_mem makes the two recursive calls it also increases the number of nonempty entries by one (filling in a formerly empty entry in the table with a new value). Since the table has only n entries, there can thus only be a total of O(n) calls to f_mem, for a total running time of O(n) (because we established above that each call takes O(1) time). This speedup from memoization thus reduces the running time from exponential to linear, a huge change—e.g., for n=4 the speedup from memoization is more than a factor of a million!
进度度量的一个很好的选择,不仅在这里,而且对于记忆的许多用途来说,是表中非空条目的数量(即包含 Some n 而不是 None 的条目)。每次 f_mem 进行两次递归调用时,它也会将非空条目的数量增加一(用新值填充表中以前为空的条目)。由于该表只有 n 条目,因此总共只能有 O(n) 次调用 f_mem ,总运行时间为 O(n) 时间)。因此,记忆化的加速将运行时间从指数减少到线性,这是一个巨大的变化 - 例如,对于 n=4 记忆化的加速超过一百万倍!

The key to being able to apply memoization is that there are common sub-problems which are being solved repeatedly. Thus we are able to use some extra storage to save on repeated computation.
能够应用记忆化的关键是存在不断被重复解决的常见子问题。因此,我们可以使用一些额外的存储来节省重复计算。

Although this code uses imperative constructs (specifically, array update), the side effects are not visible outside the function fibm. So from a client’s perspective, fibm is functional. There’s no need to mention the imperative implementation (i.e., the benign side effects) that are used internally.
尽管此代码使用命令式构造(具体来说,数组更新),但副作用在函数 fibm 之外不可见。因此,从客户的角度来看, fibm 是有效的。无需提及内部使用的命令式实现(即良性副作用)。

8.5.2. Memoization Using Higher-order Functions
8.5.2. 使用高阶函数进行记忆 ¶

Now that we’ve seen an example of memoizing one function, let’s use higher-order functions to memoize any function. First, consider the case of memoizing a non-recursive function f. In that case we simply need to create a hash table that stores the corresponding value for each argument that f is called with (and to memoize multi-argument functions we can use currying and uncurrying to convert to a single argument function).
现在我们已经看到了记忆一个函数的示例,让我们使用高阶函数来记忆任何函数。首先,考虑记忆非递归函数 f 的情况。在这种情况下,我们只需要创建一个哈希表来存储调用 f 的每个参数的相应值(并且为了记住多参数函数,我们可以使用柯里化和非柯里化来转换为单个参数功能)。

let memo f =
  let h = Hashtbl.create 11 in
  fun x ->
    try Hashtbl.find h x
    with Not_found ->
      let y = f x in
      Hashtbl.add h x y;
      y
val memo : ('a -> 'b) -> 'a -> 'b = <fun>

For recursive functions, however, the recursive call structure needs to be modified. This can be abstracted out independent of the function that is being memoized:
然而,对于递归函数,需要修改递归调用结构。这可以独立于正在记忆的函数而被抽象出来:

let memo_rec f =
  let h = Hashtbl.create 16 in
  let rec g x =
    try Hashtbl.find h x
    with Not_found ->
      let y = f g x in
      Hashtbl.add h x y;
      y
  in
  g
val memo_rec : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>

Now we can slightly rewrite the original fib function above using this general memoization technique:
现在我们可以使用这种通用记忆技术稍微重写上面的原始 fib 函数:

let fib_memo =
  let rec fib self n =
    if n < 2 then 1 else self (n - 1) + self (n - 2)
  in
  memo_rec fib
val fib_memo : int -> int = <fun>

8.5.3. Just for Fun: Party Optimization
8.5.3. 只是为了好玩:派对优化 ¶

Suppose we want to throw a party for a company whose org chart is a binary tree. Each employee has an associated “fun value” and we want the set of invited employees to have a maximum total fun value. However, no employee is fun if his superior is invited, so we never invite two employees who are connected in the org chart. (The less fun name for this problem is the maximum weight independent set in a tree.) For an org chart with n employees, there are 2n possible invitation lists, so the naive algorithm that compares the fun of every valid invitation list takes exponential time.
假设我们想为一家组织结构图为二叉树的公司举办一场聚会。每个员工都有一个相关的“乐趣值”,我们希望受邀员工的集合具有最大的总乐趣值。然而,如果邀请他的上级,没有一个员工会感到有趣,所以我们从不邀请两个在组织结构图中有联系的员工。 (此问题的不太有趣的名称是树中的最大权重独立集。)对于具有 n 员工的组织结构图,有 2n 个可能的邀请列表,因此朴素算法相比之下,每个有效邀请列表的乐趣都需要指数级的时间。

We can use memoization to turn this into a linear-time algorithm. We start by defining a variant type to represent the employees. The int at each node is the fun.
我们可以使用记忆将其转化为线性时间算法。我们首先定义一个代表员工的变体类型。每个节点的 int 很有趣。

type tree = Empty | Node of int * tree * tree

Now, how can we solve this recursively? One important observation is that in any tree, the optimal invitation list that doesn’t include the root node will be the union of optimal invitation lists for the left and right subtrees. And the optimal invitation list that does include the root node will be the union of optimal invitation lists for the left and right children that do not include their respective root nodes. So it seems useful to have functions that optimize the invite lists for the case where the root node is required to be invited, and for the case where the root node is excluded. We’ll call these two functions party_in and party_out. Then the result of party is just the maximum of these two functions:
现在,我们如何递归地解决这个问题?一个重要的观察是,在任何树中,不包括根节点的最佳邀请列表将是左子树和右子树的最佳邀请列表的并集。并且包含根节点的最优邀请列表将是不包含各自根节点的左子节点和右子节点的最优邀请列表的并集。因此,对于需要邀请根节点的情况以及排除根节点的情况,拥有优化邀请列表的功能似乎很有用。我们将这两个函数称为 party_in 和 party_out。那么 party 的结果就是这两个函数的最大值:

module Unmemoized = struct
  type tree =
    | Empty
    | Node of int * tree * tree

  (* Returns optimum fun for t. *)
  let rec party t = max (party_in t) (party_out t)

  (* Returns optimum fun for t assuming the root node of t
   * is included. *)
  and party_in t =
    match t with
    | Empty -> 0
    | Node (v, left, right) -> v + party_out left + party_out right

  (* Returns optimum fun for t assuming the root node of t
   * is excluded. *)
  and party_out t =
    match t with
    | Empty -> 0
    | Node (v, left, right) -> party left + party right
end
module Unmemoized :
  sig
    type tree = Empty | Node of int * tree * tree
    val party : tree -> int
    val party_in : tree -> int
    val party_out : tree -> int
  end

This code has exponential running time. But notice that there are only n possible distinct calls to party. If we change the code to memoize the results of these calls, the performance will be linear in n. Here is a version that memoizes the result of party and also computes the actual invitation lists. Notice that this code memoizes results directly in the tree.
该代码的运行时间呈指数级增长。但请注意,只有 n 可能是不同的派对呼叫。如果我们更改代码以记住这些调用的结果,则 n 的性能将呈线性。这是一个记录聚会结果并计算实际邀请列表的版本。请注意,此代码直接在树中记忆结果。

module Memoized = struct
  (* This version memoizes the optimal fun value for each tree node. It
     also remembers the best invite list. Each tree node has the name of
     the employee as a string. *)
  type tree =
    | Empty
    | Node of
        int * string * tree * tree * (int * string list) option ref

  let rec party t : int * string list =
    match t with
    | Empty -> (0, [])
    | Node (_, name, left, right, memo) -> (
        match !memo with
        | Some result -> result
        | None ->
            let infun, innames = party_in t in
            let outfun, outnames = party_out t in
            let result =
              if infun > outfun then (infun, innames)
              else (outfun, outnames)
            in
            memo := Some result;
            result)

  and party_in t =
    match t with
    | Empty -> (0, [])
    | Node (v, name, l, r, _) ->
        let lfun, lnames = party_out l and rfun, rnames = party_out r in
        (v + lfun + rfun, name :: lnames @ rnames)

  and party_out t =
    match t with
    | Empty -> (0, [])
    | Node (_, _, l, r, _) ->
        let lfun, lnames = party l and rfun, rnames = party r in
        (lfun + rfun, lnames @ rnames)
end
module Memoized :
  sig
    type tree =
        Empty
      | Node of int * string * tree * tree * (int * string list) option ref
    val party : tree -> int * string list
    val party_in : tree -> int * string list
    val party_out : tree -> int * string list
  end

Why was memoization so effective for solving this problem? As with the Fibonacci algorithm, we had the overlapping sub-problems property, in which the naive recursive implementation called the function party many times with the same arguments. Memoization saves all those calls. Further, the party optimization problem has the property of optimal substructure, meaning that the optimal answer to a problem is computed from optimal answers to sub-problems. Not all optimization problems have this property. The key to using memoization effectively for optimization problems is to figure out how to write a recursive function that implements the algorithm and has two properties. Sometimes this requires thinking carefully.
为什么记忆对于解决这个问题如此有效?与斐波那契算法一样,我们具有重叠子问题属性,其中朴素的递归实现使用相同的参数多次调用函数 party。记忆功能可以保存所有这些调用。此外,参与方优化问题具有最优子结构的属性,这意味着问题的最优答案是根据子问题的最优答案计算出来的。并非所有优化问题都具有此属性。有效地使用记忆化来解决优化问题的关键是弄清楚如何编写一个实现该算法并具有两个属性的递归函数。有时这需要仔细思考。

8.6. Promises 8.6. 承诺 ¶

So far we have only considered sequential programs. Execution of a sequential program proceeds one step at a time, with no choice about which step to take next. Sequential programs are limited in that they are not very good at dealing with multiple sources of simultaneous input and they can only execute on a single processor. Many modern applications are instead concurrent.
到目前为止,我们只考虑了顺序程序。顺序程序的执行一次执行一步,无法选择下一步要执行哪一步。顺序程序的局限性在于它们不太擅长处理多个同时输入源,并且只能在单个处理器上执行。许多现代应用程序都是并发的。

8.6.1. Concurrency 8.6.1. 并发 ¶

Concurrent programs enable computations to overlap in duration, instead of being forced to happen sequentially.
并发程序使计算能够在持续时间内重叠,而不是被迫按顺序发生。

  • Graphical user interfaces (GUIs), for example, rely on concurrency to keep the interface responsive while computation continues in the background. Without concurrency, a GUI would “lock up” until the current action is completed. Sometimes, because of concurrency bugs, that happens anyway—and it’s frustrating for the user!
    例如,图形用户界面 (GUI) 依靠并发来保持界面响应,同时计算在后台继续进行。如果没有并发性,GUI 将“锁定”,直到当前操作完成。有时,由于并发错误,这种情况无论如何都会发生——这对用户来说是令人沮丧的!

  • A spreadsheet needs concurrency to re-compute all the cells while still keeping the menus and editing capabilities available for the user.
    电子表格需要并发性来重新计算所有单元格,同时仍然保持用户可用的菜单和编辑功能。

  • A web browser needs concurrency to read and render web pages incrementally as new data comes in over the network, to run JavaScript programs embedded in the web page, and to enable the user to navigate through the page and click on hyperlinks.
    当新数据通过网络传入时,Web 浏览器需要并发性来增量读取和呈现网页,运行嵌入在网页中的 JavaScript 程序,并使用户能够浏览页面并单击超链接。

Servers are another example of applications that need concurrency. A web server needs to respond to many requests from clients, and clients would prefer not to wait. If an assignment is released in CMS, for example, you would prefer to be able to view that assignment at the same time as everyone else in the class, rather than having to “take a number” a wait for your number to be called—as at the Department of Motor Vehicles, or at an old-fashioned deli, etc.
服务器是需要并发的应用程序的另一个示例。 Web 服务器需要响应来自客户端的许多请求,而客户端不希望等待。例如,如果作业在 CMS 中发布,您希望能够与班级中的其他人同时查看该作业,而不是必须“取号”等待呼叫您的号码——如在机动车辆管理局或老式熟食店等。

One of the primary jobs of an operating system (OS) is to provide concurrency. The OS makes it possible for many applications to be executing concurrently: a music player, a web browser, a code editor, etc. How does it do that? There are two fundamental, complementary approaches:
操作系统 (OS) 的主要工作之一是提供并发性。该操作系统使许多应用程序可以同时执行:音乐播放器、网络浏览器、代码编辑器等。它是如何做到这一点的?有两种基本的、互补的方法:

  • Interleaving: rapidly switch back and forth between computations. For example, execute the music player for 100 milliseconds, then the browser, then the editor, then repeat. That makes it appear as though multiple computations are occurring simultaneously, but in reality, only one is ever occurring at the same time.
    交错能力:在计算之间快速来回切换。例如,执行音乐播放器 100 毫秒,然后是浏览器,然后是编辑器,然后重复。这使得多项计算看起来好像同时发生,但实际上,同时只发生一项。

  • Parallelism: use hardware that is capable of performing two or more computations literally at the same time. Many processors these days are multicore, meaning that they have multiple central processing units (CPUs), each of which can be executing a program simultaneously.
    并行能力:使用能够同时执行两个或多个计算的硬件。如今,许多处理器都是多核的,这意味着它们具有多个中央处理单元 (CPU),每个中央处理单元都可以同时执行一个程序。

Regardless of the approaches being used, concurrent programming is challenging. Even if there are multiple cores available for simultaneous use, there are still many other resources that must be shared: memory, the screen, the network interface, etc. Managing that sharing, especially without introducing bugs, is quite difficult. For example, if two programs want to communicate by using the computer’s memory, there needs to be some agreement on when each program is allowed to read and write from the memory. Otherwise, for example, both programs might attempt to write to the same location in memory, leading to corrupted data. Those kinds of race conditions, where a program races to complete its operations before another program, are notoriously difficult to avoid.
无论使用哪种方法,并发编程都是具有挑战性的。即使有多个核心可供同时使用,仍然有许多其他资源必须共享:内存、屏幕、网络接口等。管理这种共享,特别是在不引入错误的情况下,是相当困难的。例如,如果两个程序想要使用计算机的内存进行通信,则需要就何时允许每个程序从内存中读取和写入达成某种协议。否则,例如,两个程序可能会尝试写入内存中的同一位置,从而导致数据损坏。众所周知,这种竞争条件(即一个程序在另一个程序之前完成其操作)是很难避免的。

The most fundamental challenge is that concurrency makes the execution of a program become nondeterministic: the order in which operations occur cannot necessarily be known ahead of time. Race conditions are an example of nondeterminism. To program correctly in the face of nondeterminism, the programmer is forced to think about all possible orders in which operations might execute, and ensure that in all of them the program works correctly.
最基本的挑战是并发性使程序的执行变得不确定:不一定能够提前知道操作发生的顺序。竞争条件是非确定性的一个例子。为了在面对不确定性时正确编程,程序员被迫考虑操作可能执行的所有可能顺序,并确保程序在所有这些顺序中都能正确运行。

Purely functional programs make nondeterminism easier to reason about, because evaluation of an expression always returns the same value no matter what. For example, in the expression (2 * 4) + (3 * 5), the operations can be executed concurrently (e.g., with the left and right products evaluated simultaneously) without changing the answer. Imperative programming is more problematic. For example, the expressions !x and incr x; !x, if executed concurrently, could give different results depending on which executes first.
纯函数式程序使非确定性更容易推理,因为无论如何,表达式的计算总是返回相同的值。例如,在表达式 (2 * 4) + (3 * 5) 中,操作可以并发执行(例如,同时评估左右乘积)而不改变答案。命令式编程的问题较多。例如,表达式 !xincr x; !x 如果同时执行,可能会给出不同的结果,具体取决于哪个先执行。

8.6.2. Threads 8.6.2. 线程 ¶

To make concurrent programming easier, computer scientists have invented many abstractions. One of the best known is threads. Abstractly, a thread is a single sequential computation. There can be many threads running at a time, either interleaved or in parallel depending on the hardware, and a scheduler handles choosing which threads are running at any given time. Scheduling can either be preemptive, meaning that the scheduler is permitted to stop a thread and restart it later without the thread getting a choice in the matter, or cooperative, meaning that the thread must choose to relinquish control back to the scheduler. The former can lead to race conditions, and the latter can lead to unresponsive applications.
为了使并发编程变得更容易,计算机科学家发明了许多抽象概念。最著名的之一是线程。抽象地讲,线程是单个顺序计算。一次可以有多个线程运行,根据硬件的不同,可以是交错的,也可以是并行的,并且调度程序负责选择在任何给定时间运行哪些线程。调度可以是抢占式的,这意味着调度程序可以停止线程并稍后重新启动它,而线程无需做出选择;也可以是协作式的,这意味着线程必须选择将控制权交还给调度程序。前者可能导致竞争条件,后者可能导致应用程序无响应。

Concretely, a thread is a set of values that are loaded into the registers of a processor. Those values tell the processor where to find the next instruction to execute, where its stack and heap are located in memory, etc. To implement preemption, a scheduler sets a timer in the hardware; when the timer goes off, the current thread is interrupted and the scheduler gets to run. CS 3410 and 4410 cover those concepts in detail.
具体来说,线程是加载到处理器寄存器中的一组值。这些值告诉处理器在哪里找到下一条要执行的指令,其堆栈和堆位于内存中的位置等。为了实现抢占,调度程序在硬件中设置一个计时器;当计时器关闭时,当前线程被中断并且调度程序开始运行。 CS 3410 和 4410 详细介绍了这些概念。

8.6.3. Promises 8.6.3. 承诺 ¶

In the functional programming paradigm, one of the best known abstractions for concurrency is promises. Other names for this idea include futures, deferreds, and delayeds. All those names refer to the idea of a computation that is not yet finished: it has promised to eventually produce a value in the future, but the completion of the computation has been deferred or delayed. There may be many such values being computed concurrently, and when the value is finally available, there may be computations ready to execute that depend on the value.
在函数式编程范式中,最著名的并发抽象之一是承诺。这个想法的其他名称包括期货、延期和延迟。所有这些名称都指的是尚未完成的计算的想法:它承诺最终会在未来产生一个值,但计算的完成已被推迟或延迟。可能有许多这样的值被同时计算,并且当该值最终可用时,可能存在准备好执行的取决于该值的计算。

This idea has been widely adopted in many languages and libraries, including Java, JavaScript, and .NET. Indeed, modern JavaScript adds an async keyword that causes a function to return a promise, and an await keyword that waits for a promise to finish computing. There are two widely-used libraries in OCaml that implement promises: Async and Lwt. Async is developed by Jane Street. Lwt is part of the Ocsigen project, which is a web framework for OCaml.
这种思想已被许多语言和库广泛采用,包括 Java、JavaScript 和 .NET。事实上,现代 JavaScript 添加了一个 async 关键字,使函数返回一个 Promise,以及一个 await 关键字,等待 Promise 完成计算。 OCaml 中有两个广泛使用的库来实现 Promise:Async 和 Lwt。 Async 由 Jane Street 开发。 Lwt 是 Ocsigen 项目的一部分,Ocsigen 项目是 OCaml 的 Web 框架。

We now take a deeper look at promises in Lwt. The name of the library was an acronym for “light-weight threads.” But that was a misnomer, as the GitHub page admits (as of 10/22/18):
我们现在更深入地研究 Lwt 中的承诺。该库的名称是“轻量级线程”的缩写。但正如 GitHub 页面所承认的那样,这是一个用词不当(截至 2018 年 10 月 22 日):

Much of the current manual refers to … “lightweight threads” or just “threads.” This will be fixed in the new manual. [Lwt implements] promises, and has nothing to do with system or preemptive threads.
当前手册的大部分内容都提到……“轻量级线程”或只是“线程”。这将在新手册中修复。( Lwt 中实现的) Promise 与系统或抢占式线程无关。

So don’t think of Lwt as having anything to do with threads: it really is a library for promises.
因此,不要认为 Lwt 与线程有任何关系:它实际上是一个 Promise 库。

In Lwt, a promise is a write-once reference: a value that is permitted to mutate at most once. When created, it is like an empty box that contains nothing. We say that the promise is pending. Eventually the promise can be resolved, which is like putting something inside the box. Instead of being resolved, the promise can instead be rejected, in which case the box is filled with an exception. Regardless of whether the promise is resolved or rejected, once the box is filled, its contents may never change.
在 Lwt 中,promise 是一次写入引用:最多允许改变一次的值。创建后,它就像一个空盒子,里面什么也没有。我们说承诺尚未完成。最终这个承诺可以得到解决,这就像把东西放进盒子里一样。承诺不会被解决,而是会被拒绝,在这种情况下,该框会填充异常。无论承诺是被解决还是被拒绝,一旦盒子被填满,它的内容可能永远不会改变。

For now, we will mostly forget about concurrency. Later we’ll come back and incorporate it. But there is one part of the design for concurrency that we need to address now. When we later start using functions for OS-provided concurrency, such as concurrent reads and writes from files, there will need to be a division of responsibilities:
现在,我们基本上会忘记并发性。稍后我们会回来并合并它。但我们现在需要解决并发设计的一部分。当我们稍后开始使用操作系统提供的并发功能时,例如文件的并发读写,就需要划分职责:

  • The client code that wants to make use of concurrency will need to access promises: query whether they are resolved or pending, and make use of the resolved values.
    想要利用并发性的客户端代码需要访问 Promise:查询它们是否已解决或待处理,并使用已解决的值。

  • The library and OS code that implements concurrency will need to mutate the promise—that is, to actually resolve or reject it. Client code does not need that ability.
    实现并发的库和操作系统代码需要改变承诺,即实际解决或拒绝它。客户端代码不需要这种能力。

We therefore will introduce one additional abstraction called a resolver. There will be a one-to-one association between promises and resolvers. The resolver for a promise will be used internally by the concurrency library but not revealed to clients. The clients will only get access to the promise.
因此,我们将引入一种额外的抽象,称为解析器。承诺和解析器之间将存在一对一的关联。 Promise 的解析器将由并发库内部使用,但不会透露给客户端。客户只能获得承诺。

For example, suppose the concurrency library supported a operation to concurrently read a string from the network. The library would implement that operation as follows:
例如,假设并发库支持从网络并发读取字符串的操作。该库将按如下方式实现该操作:

  • Create a new promise and its associated resolver. The promise is pending.
    创建一个新的 Promise 及其关联的解析器。该承诺尚未确定。

  • Call an OS function that will concurrently read the string then invoke the resolver on that string.
    调用将同时读取字符串的操作系统函数,然后调用该字符串的解析器。

  • Return the promise (but not resolver) to the client. The OS meanwhile continues to work on reading the string.
    将承诺(但不包括解析器)返回给客户端。与此同时,操作系统继续读取字符串。

You might think of the resolver as being a “private and writeable” value used primarily by the library and the promise as being a “public and read-only” value used primarily by the client.
您可能会认为解析器是主要由库使用的“私有且可写”值,而承诺是主要由客户端使用的“公共且只读”值。

8.6.4. Making Our Own Promises
8.6.4. 做出我们自己的承诺 ¶

Here is an interface for our own Lwt-style promises. The names have been changed to make the interface clearer.
这是我们自己的 Lwt 风格的 Promise 的接口。名称已更改以使界面更清晰。

(** A signature for Lwt-style promises, with better names *)
module type PROMISE = sig
  type 'a state =
    | Pending
    | Resolved of 'a
    | Rejected of exn

  type 'a promise

  type 'a resolver

  (** [make ()] is a new promise and resolver. The promise is pending. *)
  val make : unit -> 'a promise * 'a resolver

  (** [return x] is a new promise that is already resolved with value
      [x]. *)
  val return : 'a -> 'a promise

  (** [state p] is the state of the promise *)
  val state : 'a promise -> 'a state

  (** [resolve r x] resolves the promise [p] associated with [r] with
      value [x], meaning that [state p] will become [Resolved x].
      Requires: [p] is pending. *)
  val resolve : 'a resolver -> 'a -> unit

  (** [reject r x] rejects the promise [p] associated with [r] with
      exception [x], meaning that [state p] will become [Rejected x].
      Requires: [p] is pending. *)
  val reject : 'a resolver -> exn -> unit
end
module type PROMISE =
  sig
    type 'a state = Pending | Resolved of 'a | Rejected of exn
    type 'a promise
    type 'a resolver
    val make : unit -> 'a promise * 'a resolver
    val return : 'a -> 'a promise
    val state : 'a promise -> 'a state
    val resolve : 'a resolver -> 'a -> unit
    val reject : 'a resolver -> exn -> unit
  end

To implement that interface, we can make the representation type of 'a promise be a reference to a state:
要实现该接口,我们可以使 'a promise 的表示类型成为对状态的引用:

type 'a state = Pending | Resolved of 'a | Rejected of exn
type 'a promise = 'a state ref
type 'a state = Pending | Resolved of 'a | Rejected of exn
type 'a promise = 'a state ref

That way it’s possible to mutate the contents of the promise.
这样就可以改变承诺的内容。

For the representation type of the resolver, we’ll do something a little clever. It will simply be the same as a promise.
对于解析器的表示类型,我们会做一些聪明的事情。它就像一个承诺一样。

type 'a resolver = 'a promise
type 'a resolver = 'a promise

So internally, the two types are exactly the same. But externally no client of the Promise module will be able to distinguish them. In other words, we’re using the type system to control whether it’s possible to apply certain functions (e.g., state vs resolve) to a promise.
所以在内部,这两种类型是完全相同的。但在外部, Promise 模块的客户端无法区分它们。换句话说,我们使用类型系统来控制是否可以将某些函数(例如 stateresolve )应用于 Promise。

To help implement the rest of the functions, let’s start by writing a helper function update : 'a promise -> 'a state -> unit to update the reference. This function will implement changing the state of the promise from pending to either resolved or rejected, and once the state has changed, it will not allow it to be changed again. In other words, update enforces the “write once” invariant.
为了帮助实现其余功能,我们首先编写一个辅助函数 update : 'a promise -> 'a state -> unit 来更新引用。该函数将实现将promise的状态从pending更改为resolved或rejected,并且一旦状态改变,就不允许再次更改。换句话说, update 强制执行“一次写入”不变式。

(** [write_once p s] changes the state of [p] to be [s].  If [p] and [s]
    are both pending, that has no effect.
    Raises: [Invalid_arg] if the state of [p] is not pending. *)
let write_once p s =
  if !p = Pending
  then p := s
  else invalid_arg "cannot write twice"
val write_once : 'a state ref -> 'a state -> unit = <fun>

Using that helper, we can implement the make function:
使用该助手,我们可以实现 make 函数:

let make () =
  let p = ref Pending in
  p, p
val make : unit -> 'a state ref * 'a state ref = <fun>

The remaining functions in the interface are trivial to implement. Putting it altogether in a module, we have:
接口中的其余功能实现起来很简单。将其全部放在一个模块中,我们有:

module Promise : PROMISE = struct
  type 'a state =
    | Pending
    | Resolved of 'a
    | Rejected of exn

  type 'a promise = 'a state ref

  type 'a resolver = 'a promise

  (** [write_once p s] changes the state of [p] to be [s]. If [p] and
      [s] are both pending, that has no effect. Raises: [Invalid_arg] if
      the state of [p] is not pending. *)
  let write_once p s =
    if !p = Pending then p := s else invalid_arg "cannot write twice"

  let make () =
    let p = ref Pending in
    (p, p)

  let return x = ref (Resolved x)

  let state p = !p

  let resolve r x = write_once r (Resolved x)

  let reject r x = write_once r (Rejected x)
end
module Promise : PROMISE

8.6.5. Lwt Promises 8.6.5. Lwt 承诺 ¶

The types and names used in Lwt are a bit more obscure than those we used above. Lwt uses analogical terminology that comes from threads—but since Lwt does not actually implement threads, that terminology is not necessarily helpful. (We don’t mean to demean Lwt! It is a library that has been developing and changing over time.)
Lwt 中使用的类型和名称比我们上面使用的要晦涩一些。 Lwt 使用来自线程的类比术语,但由于 Lwt 实际上并不实现线程,因此该术语不一定有帮助。 (我们并不是有意贬低 Lwt!它是一个随着时间的推移而不断发展和变化的库。)

The Lwt interface includes the following declarations, which we have annotated with comments to compare them to the interface we implemented above:
Lwt 接口包括以下声明,我们用注释对其进行了注释,以便将它们与我们上面实现的接口进行比较:

module type Lwt = sig
  (* [Sleep] means pending.  [Return] means resolved.
     [Fail] means rejected. *)
  type 'a state = Sleep | Return of 'a | Fail of exn

  (* a [t] is a promise *)
  type 'a t

  (* a [u] is a resolver *)
  type 'a u

  val state : 'a t -> 'a state

  (* [wakeup] means [resolve] *)
  val wakeup : 'a u -> 'a -> unit

  (* [wakeup_exn] means [reject] *)
  val wakeup_exn : 'a u -> exn -> unit

  (* [wait] means [make] *)
  val wait : unit -> 'a t * 'a u

  val return : 'a -> 'a t
end
module type Lwt =
  sig
    type 'a state = Sleep | Return of 'a | Fail of exn
    type 'a t
    type 'a u
    val state : 'a t -> 'a state
    val wakeup : 'a u -> 'a -> unit
    val wakeup_exn : 'a u -> exn -> unit
    val wait : unit -> 'a t * 'a u
    val return : 'a -> 'a t
  end

Lwt’s implementation of that interface is much more complex than our own implementation above, because Lwt actually supports many more operations on promises. Nonetheless, the core ideas that we developed above provide sound intuition for what Lwt implements.
Lwt 的该接口的实现比我们上面自己的实现复杂得多,因为 Lwt 实际上支持更多的 Promise 操作。尽管如此,我们上面提出的核心思想为 Lwt 的实现提供了良好的直觉。

Here is some example Lwt code that you can try out in utop:
以下是一些 Lwt 代码示例,您可以在 utop 中尝试:

#require "lwt";;
let p, r = Lwt.wait();;
val p : '_weak1 Lwt.t = <abstr>
val r : '_weak1 Lwt.u = <abstr>

To avoid those weak type variables, we can provide a further hint to OCaml as to what type we want to eventually put into the promise. For example, if we wanted to have a promise that will eventually contain an int, we could write this code:
为了避免这些弱类型变量,我们可以向 OCaml 提供进一步的提示,说明我们最终希望将什么类型放入 Promise 中。例如,如果我们想要一个最终包含 int 的 Promise,我们可以编写以下代码:

let (p : int Lwt.t), r = Lwt.wait ()
val p : int Lwt.t = <abstr>
val r : int Lwt.u = <abstr>

Now we can resolve the promise:
现在我们可以解决这个承诺:

Lwt.state p
- : int Lwt.state = Lwt.Sleep
Lwt.wakeup r 42
- : unit = ()
Lwt.state p;;
- : int Lwt.state = Lwt.Return 42
Lwt.wakeup r 42
Exception: Invalid_argument "Lwt.wakeup".
Raised at Stdlib.invalid_arg in file "stdlib.ml", line 30, characters 20-45
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

That last exception was raised because we attempted to resolve the promise a second time, which is not permitted.
引发最后一个异常是因为我们试图第二次解决承诺,这是不允许的。

To reject a promise, we can write similar code:
要拒绝承诺,我们可以编写类似的代码:

let (p : int Lwt.t), r = Lwt.wait ();;
Lwt.wakeup_exn r (Failure "nope");;
Lwt.state p;;
val p : int Lwt.t = <abstr>
val r : int Lwt.u = <abstr>
- : unit = ()
- : int Lwt.state = Lwt.Fail (Failure "nope")

Note that nothing we have implemented so far does anything concurrently. The promise abstraction by itself is not inherently concurrent. It’s just a data structure that can be written at most once, and that provides a means to control who can write to it (through the resolver).
请注意,到目前为止,我们尚未实现任何同时执行任何操作的功能。 Promise 抽象本身并不是本质上并发的。它只是一个最多只能写入一次的数据结构,并且提供了一种控制谁可以写入它的方法(通过解析器)。

8.6.6. Asynchronous I/O
8.6.6. 异步 I/O ¶

Now that we understand promises as a data abstraction, let’s turn to how they can be used for concurrency. The typical way they’re used with Lwt is for concurrent input and output (I/O).
现在我们已经将 Promise 理解为一种数据抽象,接下来让我们看看如何将它们用于并发性。它们与 Lwt 一起使用的典型方式是并发输入和输出 (I/O)。

The I/O functions that are part of the OCaml standard library are synchronous aka blocking: when you call such a function, it does not return until the I/O has been completed. “Synchronous” here refers to the synchronization between your code and the I/O function: your code does not get to execute again until the I/O code is done. “Blocking” refers to the fact that your code has to wait—it is blocked—until the I/O completes.
OCaml 标准库中的 I/O 函数是同步的,也称为阻塞:当您调用此类函数时,它不会返回,直到 I/O 完成。这里的“同步”指的是你的代码和I/O函数之间的同步:在I/O代码完成之前你的代码不会再次执行。 “阻塞”是指您的代码必须等待(它被阻塞)直到 I/O 完成。

For example, the Stdlib.input_line : in_channel -> string function reads characters from an input channel until it reaches a newline character, then returns the characters it read. The type in_channel is abstract; it represents a source of data that can be read, such as a file, or the network, or the keyboard. The value Stdlib.stdin : in_channel represents the standard input channel, which is the channel which usually, by default, provides keyboard input.
例如, Stdlib.input_line : in_channel -> string 函数从输入通道读取字符,直到到达换行符,然后返回它读取的字符。类型 in_channel 是抽象的;它代表可以读取的数据源,例如文件、网络或键盘。值 Stdlib.stdin : in_channel 表示标准输入通道,通常默认情况下提供键盘输入的通道。

If you run the following code in utop, you will observe the blocking behavior:
如果您在 utop 中运行以下代码,您将观察到阻塞行为:

# ignore(input_line stdin); print_endline "done";;
<type your own input here>
done
- : unit = ()

The string "done" is not printed until after the input operation completes, which happens after you type Enter.
字符串 "done" 直到输入操作完成(输入 Enter 后发生)后才会打印。

Synchronous I/O makes it impossible for a program to carry on other computations while it is waiting for the I/O operation to complete. For some programs that’s just fine. A text adventure game, for example, doesn’t have any background computations it needs to perform. But other programs, like spreadsheets or servers, would be improved by being able to carry on computations in the background rather than having to completely block while waiting for input.
同步 I/O 使得程序在等待 I/O 操作完成时无法进行其他计算。对于某些程序来说这很好。例如,文本冒险游戏不需要执行任何后台计算。但其他程序,如电子表格或服务器,可以通过在后台进行计算而得到改进,而不必在等待输入时完全阻塞。

Asynchronous aka non-blocking I/O is the opposite style of I/O. Asynchronous I/O operations return immediately, regardless of whether the input or output has been completed. That enables a program to launch an I/O operation, carry on doing other computations, and later come back to make use of the completed operation.
异步(又称为非阻塞 I/O)是 I/O 的相反风格。异步 I/O 操作立即返回,无论输入或输出是否已完成。这使得程序能够启动 I/O 操作,继续执行其他计算,然后返回以使用已完成的操作。

The Lwt library provides its own I/O functions in the Lwt_io module, which is in the lwt.unix package. The function Lwt_io.read_line : Lwt_io.input_channel -> string Lwt.t is the asynchronous equivalent of Stdlib.input_line. Similarly, Lwt_io.input_channel is the equivalent of the OCaml standard library’s in_channel, and Lwt_io.stdin represents the standard input channel.
Lwt 库在 Lwt_io 模块中提供了自己的 I/O 函数,该模块位于 lwt.unix 包中。函数 Lwt_io.read_line : Lwt_io.input_channel -> string Lwt.tStdlib.input_line 的异步等效项。同样, Lwt_io.input_channel 相当于 OCaml 标准库的 in_channelLwt_io.stdin 表示标准输入通道。

Run this code in utop to observe the non-blocking behavior:
在 utop 中运行此代码以观察非阻塞行为:

# #require "lwt.unix";;
# open Lwt_io;;
# ignore(read_line stdin); printl "done";;
done
- : unit = ()
# <type your own input here>

The string "done" is printed immediately by Lwt_io.printl, which is Lwt’s equivalent of Stdlib.print_endline, before you even type. Note that it’s best to use just one library’s I/O functions, rather than mix them together.
在您键入之前,字符串 "done" 会立即由 Lwt_io.printl 打印,这相当于 Lwt 的 Stdlib.print_endline 。请注意,最好只使用一个库的 I/O 函数,而不是将它们混合在一起。

When you do type your input, you don’t see it echoed to the screen, because it’s happening in the background. Utop is still executing—it is not blocked—but your input is being sent to that read_line function instead of to utop. When you finally type Enter, the input operation completes, and you are back to interacting with utop.
当您键入输入时,您不会看到它回显到屏幕上,因为它是在后台发生的。 Utop 仍在执行——它没有被阻止——但您的输入将被发送到该 read_line 函数而不是 utop。当您最终输入 Enter 时,输入操作完成,您将返回到与 utop 交互。

Now imagine that instead of reading a line asynchronously, the program was a web server reading a file to be served to a client. And instead of printing a string, the server was delivering the contents of a different file that had completed reading to a different client. That’s why asynchronous I/O can be so useful: it helps to hide latency. Here, “latency” means waiting for data to be transferred from one place to another, e.g., from disk to memory. Latency hiding is an excellent use for concurrency.
现在想象一下,该程序不是异步读取一行,而是一个 Web 服务器,读取要提供给客户端的文件。服务器不是打印字符串,而是将已完成读取的不同文件的内容传递给不同的客户端。这就是异步 I/O 如此有用的原因:它有助于隐藏延迟。这里,“延迟”是指等待数据从一个地方传输到另一个地方,例如从磁盘到内存。延迟隐藏是并发的绝佳用途。

Note that all the concurrency here is really coming from the operating system, which is what provides the underlying asynchronous I/O infrastructure. Lwt is just exposing that infrastructure to you through a library.
请注意,这里的所有并发实际上都来自操作系统,它提供了底层异步 I/O 基础设施。 Lwt 只是通过库向您公开该基础设施。

8.6.7. Promises and Asynchronous I/O
8.6.7. Promise 与异步 I/O ¶

The output type of Lwt_io.read_line is string Lwt.t, meaning that the function returns a string promise. Let’s investigate how the state of that promise evolves.
Lwt_io.read_line 的输出类型是 string Lwt.t ,这意味着该函数返回一个 string 承诺。让我们研究一下这个承诺的状态是如何演变的。

When the promise is returned from read_line, it is pending:
当承诺从 read_line 返回时,它处于待处理状态:

# let p = read_line stdin in Lwt.state p;;
- : string Lwt.state = Lwt.Sleep
# <now you have to type input and Enter to regain control of utop>

When the Enter key is pressed and input is completed, the promise returned from read_line should become resolved. For example, suppose you enter “Camels are bae”:
当按下 Enter 键并完成输入时,从 read_line 返回的 Promise 应该被解析。例如,假设您输入“Camels are bae”:

# let p = read_line stdin;;
val p : string Lwt.t = <abstr>
<now you type Camels are bae followed by Enter>
# p;;
- : string = "Camels are bae"

But, if you study that output carefully, you’ll notice something very strange just happened! After the let statement, p had type string Lwt.t, as expected. But when we evaluated p, it came back as type string. It’s as if the promise disappeared.
但是,如果您仔细研究该输出,您会发现刚刚发生了一些非常奇怪的事情!正如预期的那样,在 let 语句之后, p 的类型为 string Lwt.t 。但是当我们评估 p 时,它返回类型为 string 。就好像承诺消失了一样。

What’s actually happening is that utop has some special—and potentially confusing—functionality built into it that is related to Lwt. Specifically, whenever you try to directly evaluate a promise at the top level, utop will give you the contents of the promise, rather than the promise itself, and if the promise is not yet resolved, utop will block until the promise becomes resolved so that the contents can be returned.
实际上,utop 内置了一些与 Lwt 相关的特殊且可能令人困惑的功能。具体来说,每当您尝试在顶层直接评估 Promise 时,utop 都会为您提供 Promise 的内容,而不是 Promise 本身,如果 Promise 尚未解决,utop 将阻塞,直到 Promise 得到解决,以便可以返回内容。

So the output - : string = "Camels are bae" really means that p contains a resolved string whose value is "Camels are bae", not that p itself is a string. Indeed, the #show_val directive will show us that p is a promise:
因此输出 - : string = "Camels are bae" 实际上意味着 p 包含已解析的 string ,其值为 "Camels are bae" ,而不是 p 本身是一个 string 。事实上, #show_val 指令将向我们表明 p 是一个承诺:

# #show_val p;;
val p : string Lwt.t

To disable that feature of utop, or to re-enable it, call the function UTop.set_auto_run_lwt : bool -> unit, which changes how utop evaluates Lwt promises at the top level. You can see the behavior change in the following code:
要禁用或重新启用 utop 的该功能,请调用函数 UTop.set_auto_run_lwt : bool -> unit ,这会更改 utop 在顶层评估 Lwt Promise 的方式。您可以在以下代码中看到行为变化:

# UTop.set_auto_run_lwt false;;
- : unit = ()
<now you type Camels are bae followed by Enter>
# p;;
- : string Lwt.state = <abstr>
# Lwt.state p;;
- : string Lwt.state = Lwt.Return "Camels are bae"

If you re-enable this “auto run” feature, and directly try to evaluate the promise returned by read_line, you’ll see that it behaves exactly like synchronous I/O, i.e., Stdlib.input_line:
如果您重新启用此“自动运行”功能,并直接尝试评估 read_line 返回的承诺,您会发现它的行为与同步 I/O 完全相同,即 Stdlib.input_line

# UTop.set_auto_run_lwt true;;
- : unit = ()
# read_line stdin;;
Camels are bae
- : string = "Camels are bae"

Because of the potential confusion, we will henceforth assume that auto running is disabled. A good way to make that happen is to put the following line in your .ocamlinit file:
由于潜在的混乱,我们今后将假设自动运行被禁用。实现这一点的一个好方法是将以下行放入您的 .ocamlinit 文件中:

UTop.set_auto_run_lwt false;;

8.6.8. Callbacks 8.6.8. 回调 ¶

For a program to benefit from the concurrency provided by asynchronous I/O and promises, there needs to be a way for the program to make use of resolved promises. For example, if a web server is asynchronously reading and serving multiple files to multiple clients, the server needs a way to (i) become aware that a read has completed, and (ii) then do a new asynchronous write with the result of the read. In other words, programs need a mechanism for managing the dependencies among promises.
为了让程序受益于异步 I/O 和 Promise 提供的并发性,程序需要有一种方法来使用已解析的 Promise。例如,如果 Web 服务器异步读取多个文件并将其提供给多个客户端,则服务器需要一种方法来 (i) 意识到读取已完成,以及 (ii) 然后使用读取的结果执行新的异步写入。读。换句话说,程序需要一种机制来管理 Promise 之间的依赖关系。

The mechanism provided in Lwt is named callbacks. A callback is a function that will be run sometime after a promise has been resolved, and it will receive as input the contents of the resolved promise. Think of it like asking your friend to do some work for you: they promise to do it, and to call you back on the phone with the result of the work sometime after they’ve finished.
Lwt 中提供的机制称为回调。回调是一个在 Promise 被解决后某个时间运行的函数,它将接收已解决的 Promise 的内容作为输入。可以把它想象成要求你的朋友为你做一些工作:他们承诺会做,并在完成后的某个时候给你回电话告诉你工作的结果。

Registering a callback. Here is a function that prints a string using Lwt’s version of the printf function:
注册回调。下面是一个使用 Lwt 版本的 printf 函数打印字符串的函数:

let print_the_string str = Lwt_io.printf "The string is: %S\n" str

And here, repeated from the previous section, is our code that returns a promise for a string read from standard input:
这里,重复上一节,我们的代码返回从标准输入读取的字符串的承诺:

let p = read_line stdin

To register the printing function as a callback for that promise, we use the function Lwt.bind, which binds the callback to the promise:
要将打印函数注册为该 Promise 的回调,我们使用函数 Lwt.bind ,它将回调绑定到 Promise:

Lwt.bind p print_the_string

Sometime after p is resolved, hence contains a string, the callback function will be run with that string as its input. That causes the string to be printed.
有时在 p 被解析后,因此包含一个字符串,回调函数将使用该字符串作为其输入来运行。这会导致打印字符串。

Here’s a complete utop transcript as an example of that:
这是一个完整的 utop 成绩单作为示例:

# let print_the_string str = Lwt_io.printf "The string is: %S\n" str;;
val print_the_string : string -> unit Lwt.t = <fun>
# let p = read_line stdin in Lwt.bind p print_the_string;;
- : unit Lwt.t = <abstr>
  <type Camels are bae followed by Enter>
# The string is: "Camels are bae"

Bind. The type of Lwt.bind is important to understand:
绑定。理解 Lwt.bind 的类型很重要:

'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t

The bind function takes a promise as its first argument. It doesn’t matter whether that promise has been resolved yet or not. As its second argument, bind takes a callback function. That callback takes an input which is the same type 'a as the contents of the promise. It’s not an accident that they have the same type: the whole idea is to eventually run the callback on the resolved promise, so the type the promise contains needs to be the same as the type the callback expects as input.
bind 函数将 Promise 作为其第一个参数。该承诺是否已兑现并不重要。 bind 采用回调函数作为其第二个参数。该回调采用与承诺内容类型 'a 相同的输入。它们具有相同的类型并不是偶然的:整个想法是最终在已解析的 Promise 上运行回调,因此 Promise 包含的类型需要与回调期望作为输入的类型相同。

After being invoked on a promise and callback, e.g., bind p c, the bind function does one of three things, depending on the state of p:
在 Promise 和回调(例如 bind p c )上调用后, bind 函数将执行以下三件事之一,具体取决于 p 的状态:

  • If p is already resolved, then c is run immediately on the contents of p. The promise that is returned might or might not be pending, depending on what c does.
    如果 p 已解析,则立即对 p 的内容运行 c 。返回的 Promise 可能处于待处理状态,也可能不处于待处理状态,具体取决于 c 执行的操作。

  • If p is already rejected, then c does not run. The promise that is returned is also rejected, with the same exception as p.
    如果 p 已被拒绝,则 c 不会运行。返回的 Promise 也会被拒绝,例外情况与 p 相同。

  • If p is pending, then bind does not wait for p to be resolved, nor for c to be run. Rather, bind just registers the callback to eventually be run when (or if) the promise is resolved. Therefore the bind function returns a new promise. That promise will become resolved when (or if) the callback completes running, sometime in the future. Its contents will be whatever contents are contained within the promise that the callback itself returns.
    如果 p 处于待处理状态,则 bind 不会等待 p 得到解析,也不会等待 c 运行。相反, bind 只是注册最终在(或如果)承诺得到解决时运行的回调。因此 bind 函数返回一个新的 Promise。当(或如果)回调完成运行时,这个承诺将在未来的某个时候得到解决。它的内容将是回调本身返回的承诺中包含的任何内容。

Note 笔记

For the first case above: The Lwt source code claims that this behavior might change in a later version: under high load, c might be registered to run later. But as of v4.1.0 that behavior has not yet been activated. So, don’t worry about it—this paragraph is just here to future-proof this discussion.
对于上面的第一种情况:Lwt 源代码声称此行为可能会在更高版本中改变:在高负载下, c 可能会注册为稍后运行。但从 v4.1.0 开始,该行为尚未激活。所以,不用担心——本段只是为了让这个讨论面向未来。

Let’s consider that final case in more detail. We have one promise of type 'a Lwt.t and two promises of type 'b Lwt.t:
让我们更详细地考虑最后一个案例。我们有一个 'a Lwt.t 类型的 Promise 和两个 'b Lwt.t 类型的 Promise:

  • The promise of type 'a Lwt.t, call it promise X, is an input to bind. It was pending when bind was called, and when bind returns.
    'a Lwt.t 类型的 Promise(称为 Promise X)是 bind 的输入。当调用 bindbind 返回时,它处于挂起状态。

  • The first promise of type 'b Lwt.t, call it promise Y, is created by bind and returned to the user. It is pending at that point.
    第一个 'b Lwt.t 类型的 Promise 称为 Promise Y,由 bind 创建并返回给用户。目前尚待解决。

  • The second promise of type 'b Lwt.t, call it promise Z, has not yet been created. It will be created later, when promise X has been resolved, and the callback has been run on the contents of X. The callback then returns promise Z. There is no guarantee about the state of Z; it might well still be pending when returned by the callback.
    第二个类型为 'b Lwt.t 的 Promise(称为 Promise Z)尚未创建。当 Promise X 被解析并且回调已经在 X 的内容上运行时,它将稍后创建。回调然后返回 Promise Z。对于 Z 的状态没有保证;当回调返回时,它很可能仍处于待处理状态。

  • When Z is finally resolved, the contents of Y are updated to be the same as the contents of Z.
    当Z最终被解析时,Y的内容被更新为与Z的内容相同。

The reason why bind is designed with this type is so that programmers can set up a sequential chain of callbacks. For example, the following code asynchronously reads one string; then when that string has been read, proceeds to asynchronously read a second string; then prints the concatenation of both strings:
bind 之所以设计成这种类型,是为了让程序员可以建立一个连续的回调链。例如,下面的代码异步读取一个字符串;然后,当该字符串被读取后,继续异步读取第二个字符串;然后打印两个字符串的串联:

Lwt.bind (read_line stdin) (fun s1 ->
  Lwt.bind (read_line stdin) (fun s2 ->
    Lwt_io.printf "%s\n" (s1^s2)));;

If you run that in utop, something slightly confusing will happen again: after you press Enter at the end of the first string, Lwt will allow utop to read one character. The problem is that we’re mixing Lwt input operations with utop input operations. It would be better to just create a program and run it from the command line.
如果你在 utop 中运行它,会再次发生一些令人困惑的事情:在第一个字符串末尾按 Enter 后,Lwt 将允许 utop 读取一个字符。问题是我们将 Lwt 输入操作与 utop 输入操作混合在一起。最好只创建一个程序并从命令行运行它。

To do that, put the following code in a file called read2.ml:
为此,请将以下代码放入名为 read2.ml 的文件中:

open Lwt_io

let p =
  Lwt.bind (read_line stdin) (fun s1 ->
    Lwt.bind (read_line stdin) (fun s2 ->
      Lwt_io.printf "%s\n" (s1^s2)))

let _ = Lwt_main.run p

We’ve added one new function: Lwt_main.run : 'a Lwt.t -> 'a. It waits for its input promise to be resolved, then returns the contents. Typically this function is called only once in an entire program, near the end of the main file; and the input to it is typically a promise whose resolution indicates that all execution is finished.
我们添加了一个新函数: Lwt_main.run : 'a Lwt.t -> 'a 。它等待其输入承诺得到解决,然后返回内容。通常,该函数在整个程序中仅在主文件末尾附近调用一次;它的输入通常是一个承诺,其决议表明所有执行都已完成。

Create a dune file:
创建沙丘文件:

(executable
 (name read2)
 (libraries lwt.unix))

And run the program, entering a couple strings:
运行程序,输入几个字符串:

dune exec ./read2.exe
My first string
My second string
My first stringMy second string

Now try removing the last line of read2.ml. You’ll see that the program exits immediately, without waiting for you to type.
现在尝试删除 read2.ml 的最后一行。您会看到程序立即退出,而不等待您输入。

Bind as an Operator. There is another syntax for bind that is used far more frequently than what we have seen so far. The Lwt.Infix module defines an infix operator written >>= that is the same as bind. That is, instead of writing bind p c you write p >>= c. This operator makes it much easier to write code without all the extra parentheses and indentations that our previous example had:
绑定为运算符。还有另一种绑定语法,它的使用频率比我们目前看到的要高得多。 Lwt.Infix 模块定义了一个中缀运算符 >>= ,与 bind 相同。也就是说,您不写 bind p c 而是写 p >>= c 。该运算符使编写代码变得更加容易,而无需我们之前示例中的所有额外括号和缩进:

open Lwt_io
open Lwt.Infix

let p =
  read_line stdin >>= fun s1 ->
  read_line stdin >>= fun s2 ->
  Lwt_io.printf "%s\n" (s1^s2)

let _ = Lwt_main.run p

The way to visually parse the definition of p is to look at each line as computing some promised value. The first line, read_line stdin >>= fun s1 -> means that a promise is created, resolved, and its contents extracted under the name s1. The second line means the same, except that its contents are named s2. The third line creates a final promise whose contents are eventually extracted by Lwt_main.run, at which point the program may terminate.
直观地解析 p 定义的方法是将每一行视为计算某个承诺值。第一行 read_line stdin >>= fun s1 -> 表示创建、解析了一个 Promise,并以 s1 名称提取其内容。第二行的含义相同,只是其内容被命名为 s2 。第三行创建一个最终承诺,其内容最终由 Lwt_main.run 提取,此时程序可能会终止。

The >>= operator is perhaps most famous from the functional language Haskell, which uses it extensively for monads. We’ll cover monads as our next major topic.
>>= 运算符可能最著名的是函数式语言 Haskell,该语言广泛将其用于 monad。我们将讨论单子作为我们的下一个主要主题。

Bind as Let Syntax. There is a syntax extension for OCaml that makes using bind even simpler than the infix operator >>=. To install the syntax extension, run the following command:
绑定至 Let 语法。 OCaml 有一个语法扩展,使使用绑定比中缀运算符 >>= 更简单。要安装语法扩展,请运行以下命令:

$ opam install lwt_ppx

(You might need to opam update followed by opam upgrade first.)
(您可能需要先 opam update ,然后再 opam upgrade 。)

With that extension, you can use a specialized let expression written let%lwt x = e1 in e2, which is equivalent to bind e1 (fun x -> e2) or e1 >>= fun x -> e2. We can rewrite our running example as follows:
通过该扩展,您可以使用专门的 let 表达式,编写为 let%lwt x = e1 in e2 ,它相当于 bind e1 (fun x -> e2)e1 >>= fun x -> e2 。我们可以重写我们的运行示例如下:

(* to compile, add lwt_ppx to the libraries in the dune file *)
open Lwt_io

let p =
  let%lwt s1 = read_line stdin in
  let%lwt s2 = read_line stdin in
  Lwt_io.printf "%s\n" (s1^s2)

let _ = Lwt_main.run p

Now the code looks pretty much exactly like what its equivalent synchronous version would be. But don’t be fooled: all the asynchronous I/O, the promises, and the callbacks are still there. Thus, the evaluation of p first registers a callback with a promise, then moves on to the the evaluation of Lwt_main.run without waiting for the first string to finish being read. To prove that to yourself, run the following code:
现在,代码看起来与它的等效同步版本几乎一模一样。但不要被愚弄了:所有异步 I/O、promise 和回调仍然存在。因此, p 的评估首先使用promise注册回调,然后继续评估 Lwt_main.run ,而不等待第一个字符串完成读取。为了向自己证明这一点,请运行以下代码:

open Lwt_io

let p =
  let%lwt s1 = read_line stdin in
  let%lwt s2 = read_line stdin in
  Lwt_io.printf "%s\n" (s1^s2)

let _ = Lwt_io.printf "Got here first\n"

let _ = Lwt_main.run p

You’ll see that “Got here first” prints before you get a chance to enter any input.
在您有机会输入任何内容之前,您会看到打印出“首先到达这里”。

Concurrent Composition. The Lwt.bind function provides a way to sequentially compose callbacks: first one callback is run, then another, then another, and so forth. There are other functions in the library for composition of many callbacks as a set. For example,
并发的组合Lwt.bind 函数提供了一种按顺序组合回调的方法:首先运行一个回调,然后运行另一个回调,然后运行另一个回调,依此类推。库中还有其他函数可将许多回调组合为一个集合。例如,

  • Lwt.join : unit Lwt.t list -> unit Lwt.t enables waiting upon multiple promises. Lwt.join ps returns a promise that is pending until all the promises in ps become resolved. You might register a callback on the return promise from the join to take care of some computation that needs all of a set of promises to be finished.
    Lwt.join : unit Lwt.t list -> unit Lwt.t 允许等待多个 Promise。 Lwt.join ps 返回一个待处理的 Promise,直到 ps 中的所有 Promise 都得到解决。您可以在 join 的返回承诺上注册一个回调,以处理一些需要完成所有一组承诺的计算。

  • Lwt.pick : 'a Lwt.t list -> 'a Lwt.t also enables waiting upon multiple promises, but Lwt.pick ps returns a promise that is pending until at least one promise in ps becomes resolved. You might register a callback on the return promise from the pick to take care of some computation that needs just one of a set of promises to be finished, but doesn’t care which one.
    Lwt.pick : 'a Lwt.t list -> 'a Lwt.t 还可以等待多个 Promise,但 Lwt.pick ps 返回一个待处理的 Promise,直到 ps 中的至少一个 Promise 得到解决。您可以在 pick 的返回承诺上注册一个回调,以处理一些只需要完成一组承诺中的一个的计算,但不关心是哪一个。

8.6.9. Implementing Callbacks
8.6.9. 实现回调 ¶

When a callback is registered with bind or one of the other syntaxes, it is added to a list of callbacks that is stored with the promise. Eventually, when the promise has been resolved, the Lwt resolution loop runs the callbacks registered for the promise. There is no guarantee about the execution order of callbacks for a promise. In other words, the execution order is nondeterministic. If the order matters, the programmer needs to use the composition operators (such as bind and join) to enforce an ordering. If the promise never becomes resolved (or is rejected), none of its callbacks will ever be run.
当使用 bind 或其他语法之一注册回调时,它将添加到与 Promise 一起存储的回调列表中。最终,当 Promise 得到解析时,Lwt 解析循环会运行为 Promise 注册的回调。无法保证 Promise 回调的执行顺序。换句话说,执行顺序是不确定的。如果顺序很重要,则程序员需要使用组合运算符(例如 bindjoin )来强制执行顺序。如果 Promise 从未被解决(或被拒绝),则其任何回调都不会运行。

Once again, it’s important to keep track of where the concurrency really comes from: the OS. There might be many asynchronous I/O operations occurring at the OS level. But at the OCaml level, the resolution loop is sequential, meaning that only one callback can ever be running at a time.
再次强调,跟踪并发性的真正来源非常重要:操作系统。操作系统级别可能会发生许多异步 I/O 操作。但在 OCaml 级别,解析循环是连续的,这意味着一次只能运行一个回调。

Finally, the resolution loop never attempts to interrupt a callback. So if the callback goes into an infinite loop, no other callback will ever get to run. That makes Lwt a cooperative concurrency mechanism, rather than preemptive.
最后,解析循环永远不会尝试中断回调。因此,如果回调进入无限循环,则其他回调将不会运行。这使得 Lwt 成为一种协作式并发机制,而不是抢占式的。

To better understand callback resolution, let’s implement it ourselves. We’ll use the Promise data structure we developed earlier. To start, we add a bind operator to the Promise signature:
为了更好地理解回调解析,我们自己实现一下。我们将使用之前开发的 Promise 数据结构。首先,我们向 Promise 签名添加一个绑定运算符:

module type PROMISE = sig
  ...

  (** [p >>= c] registers callback [c] with promise [p].
      When the promise is resolved, the callback will be run
      on the promises's contents.  If the promise is never
      resolved, the callback will never run. *)
  val (>>=) : 'a promise -> ('a -> 'b promise) -> 'b promise
end

Next, let’s re-develop the entire Promise structure. We start off just like before:
接下来,我们重新开发整个 Promise 结构。我们像以前一样开始:

module Promise : PROMISE = struct
  type 'a state = Pending | Resolved of 'a | Rejected of exn
  ...

But now to implement the representation type of promises, we use a record with mutable fields. The first field is the state of the promise, and it corresponds to the ref we used before. The second field is more interesting and is discussed below.
但现在为了实现承诺的表示类型,我们使用带有可变字段的记录。第一个字段是promise的状态,它对应于我们之前使用的 ref 。第二个字段更有趣,将在下面讨论。

  (** RI: the input may not be [Pending] *)
  type 'a handler = 'a state -> unit

  (** RI: if [state <> Pending] then [handlers = []]. *)
  type 'a promise = {
    mutable state : 'a state;
    mutable handlers : 'a handler list
  }

A handler is a new abstraction: a function that takes a non-pending state. It will be used to handle resolving and rejecting promises when their state is ready to switch away from pending. The primary use for a handler will be to run callbacks. As a representation invariant, we require that only pending promises may have handlers waiting in their list. Once the state becomes non-pending, i.e., either resolved or rejected, the handlers will all be processed and removed from the list.
处理程序是一个新的抽象:一个处于非挂起状态的函数。当承诺的状态准备好从挂起状态切换时,它将用于处理解决和拒绝承诺。处理程序的主要用途是运行回调。作为表示不变式,我们要求只有待处理的 Promise 才能在其列表中等待处理程序。一旦状态变为非挂起(即已解决或已拒绝),处理程序将全部被处理并从列表中删除。

This helper function that enqueues a handler on a promise’s handler list will be helpful later:
这个将处理程序放入 Promise 处理程序列表中的辅助函数稍后会很有帮助:

  let enqueue
      (handler : 'a state -> unit)
      (promise : 'a promise) : unit
    =
    promise.handlers <- handler :: promise.handlers

We continue to pun resolvers and promises internally:
我们继续在内部双关解析器和承诺:

  type 'a resolver = 'a promise

Because we changed the representation type from a ref to a record, we have to update a few of the functions in trivial ways:
因为我们将表示类型从 ref 更改为记录,所以我们必须以简单的方式更新一些函数:

  (** [write_once p s] changes the state of [p] to be [s].  If [p] and [s]
      are both pending, that has no effect.
      Raises: [Invalid_arg] if the state of [p] is not pending. *)
  let write_once p s =
    if p.state = Pending
    then p.state <- s
    else invalid_arg "cannot write twice"

  let make () =
    let p = {state = Pending; handlers = []} in
    p, p

  let return x =
    {state = Resolved x; handlers = []}

  let state p = p.state

Now we get to the trickier parts of the implementation. To resolve or reject a promise, the first thing we need to do is to call write_once on it, as we did before. Now we also need to process the handlers. Before doing so, we mutate the handlers list to be empty to ensure that the RI holds.
现在我们来看看实现中比较棘手的部分。要解决或拒绝一个 Promise,我们需要做的第一件事是对其调用 write_once ,就像我们之前所做的那样。现在我们还需要处理处理程序。在此之前,我们将处理程序列表更改为空,以确保 RI 成立。

  (** requires: [st] may not be [Pending] *)
  let resolve_or_reject (r : 'a resolver) (st : 'a state) =
    assert (st <> Pending);
    let handlers = r.handlers in
    r.handlers <- [];
    write_once r st;
    List.iter (fun f -> f st) handlers

  let reject r x =
    resolve_or_reject r (Rejected x)

  let resolve r x =
    resolve_or_reject r (Resolved x)

Finally, the implementation of >>= is the trickiest part. First, if the promise is already resolved, let’s go ahead and immediately run the callback on it:
最后, >>= 的实现是最棘手的部分。首先,如果 Promise 已经解决,那么让我们立即对其运行回调:

  let (>>=)
      (input_promise : 'a promise)
      (callback : 'a -> 'b promise) : 'b promise
    =
    match input_promise.state with
    | Resolved x -> callback x

Second, if the promise is already rejected, then we return a promise that is rejected with the same exception:
其次,如果 Promise 已经被拒绝,那么我们返回一个被拒绝的 Promise,并出现相同的异常:

    | Rejected exc -> {state = Rejected exc; handlers = []}

Third, if the promise is pending, we need to do more work. Here’s what we said in our discussion of bind in the previous section:
第三,如果承诺悬而未决,我们需要做更多的工作。这是我们在上一节讨论 bind 时所说的内容:

[T]he bind function returns a new promise. That promise will become resolved when (or if) the callback completes running, sometime in the future. Its contents will be whatever contents are contained within the promise that the callback itself returns.
[T]bind 函数返回一个新的 Promise。当(或如果)回调完成运行时,这个承诺将在未来的某个时候得到解决。它的内容将是回调本身返回的承诺中包含的任何内容。

That’s what we now need to implement. So, we create a new promise and resolver called output_promise and output_resolver. That promise is what bind returns. Before returning it, we use a helper function handler_of_callback (described below) to transform the callback into a handler, and enqueue that handler on the promise. That ensures the handler will be run when the promise later becomes resolved or rejected:
这就是我们现在需要实施的。因此,我们创建一个名为 output_promiseoutput_resolver 的新承诺和解析器。这个承诺就是 bind 返回的。在返回之前,我们使用辅助函数 handler_of_callback (如下所述)将回调转换为处理程序,并将该处理程序加入到 Promise 中。这确保了当 Promise 稍后得到解决或拒绝时,处理程序将运行:

    | Pending ->
      let output_promise, output_resolver = make () in
      enqueue (handler_of_callback callback output_resolver) input_promise;
      output_promise

All that’s left is to implement that helper function to create handlers from callbacks. The first two cases, below, are simple. It would violate the RI to call a handler on a pending state. And if the state is rejected, then the handler should propagate that rejection to the resolver, which causes the promise returned by bind to also be rejected.
剩下的就是实现该辅助函数以从回调创建处理程序。下面的前两种情况很简单。在挂起状态上调用处理程序会违反 RI。如果状态被拒绝,则处理程序应该将该拒绝传播到解析器,这会导致 bind 返回的 Promise 也被拒绝。

  let handler_of_callback
      (callback : 'a -> 'b promise)
      (resolver : 'b resolver) : 'a handler
    = function
      | Pending -> failwith "handler RI violated"
      | Rejected exc -> reject resolver exc

But if the state is resolved, then the callback provided by the user to bind can—at last!—be run on the contents of the resolved promise. Running the callback produces a new promise. It might already be rejected or resolved, in which case that state again propagates.
但是,如果状态已解决,则用户提供的用于绑定的回调最终可以在已解决的 Promise 的内容上运行。运行回调会产生一个新的承诺。它可能已经被拒绝或解决,在这种情况下,该状态会再次传播。

      | Resolved x ->
        let promise = callback x in
        match promise.state with
        | Resolved y -> resolve resolver y
        | Rejected exc -> reject resolver exc

But the promise might still be pending. In that case, we need to enqueue a new handler whose purpose is to do the propagation once the result is available:
但这一承诺可能仍悬而未决。在这种情况下,我们需要将一个新的处理程序加入队列,其目的是在结果可用时进行传播:

        | Pending -> enqueue (handler resolver) promise

where handler is a new helper function that creates a very simple handler to do that propagation:
其中 handler 是一个新的辅助函数,它创建一个非常简单的处理程序来执行该传播:

  let handler (resolver : 'a resolver) : 'a handler
    = function
      | Pending -> failwith "handler RI violated"
      | Rejected exc -> reject resolver exc
      | Resolved x -> resolve resolver x

The Lwt implementation of bind follows essentially the same algorithm as we just implemented. Note that there is no concurrency in bind: as we said above, everything in Lwt is sequential; it’s the OS that provides the concurrency.
bind 的 Lwt 实现基本上遵循与我们刚刚实现的算法相同的算法。请注意, bind 中没有并发:正如我们上面所说,Lwt 中的所有内容都是顺序的;是操作系统提供并发性。

8.6.10. The Full Implementation
8.6.10. 完整的实现 ¶

Here’s all of that code in one executable block:
以下是一个可执行块中的所有代码:

(** A signature for Lwt-style promises, with better names *)
module type PROMISE = sig
  type 'a state =
    | Pending
    | Resolved of 'a
    | Rejected of exn

  type 'a promise

  type 'a resolver

  (** [make ()] is a new promise and resolver. The promise is pending. *)
  val make : unit -> 'a promise * 'a resolver

  (** [return x] is a new promise that is already resolved with value
      [x]. *)
  val return : 'a -> 'a promise

  (** [state p] is the state of the promise *)
  val state : 'a promise -> 'a state

  (** [resolve r x] resolves the promise [p] associated with [r] with
      value [x], meaning that [state p] will become [Resolved x].
      Requires: [p] is pending. *)
  val resolve : 'a resolver -> 'a -> unit

  (** [reject r x] rejects the promise [p] associated with [r] with
      exception [x], meaning that [state p] will become [Rejected x].
      Requires: [p] is pending. *)
  val reject : 'a resolver -> exn -> unit

  (** [p >>= c] registers callback [c] with promise [p].
      When the promise is resolved, the callback will be run
      on the promises's contents.  If the promise is never
      resolved, the callback will never run. *)
  val (>>=) : 'a promise -> ('a -> 'b promise) -> 'b promise
end

module Promise : PROMISE = struct
  type 'a state = Pending | Resolved of 'a | Rejected of exn

  (** RI: the input may not be [Pending] *)
  type 'a handler = 'a state -> unit

  (** RI: if [state <> Pending] then [handlers = []]. *)
  type 'a promise = {
    mutable state : 'a state;
    mutable handlers : 'a handler list
  }

  let enqueue
      (handler : 'a state -> unit)
      (promise : 'a promise) : unit
    =
    promise.handlers <- handler :: promise.handlers

  type 'a resolver = 'a promise

  (** [write_once p s] changes the state of [p] to be [s].  If [p] and [s]
      are both pending, that has no effect.
      Raises: [Invalid_arg] if the state of [p] is not pending. *)
  let write_once p s =
    if p.state = Pending
    then p.state <- s
    else invalid_arg "cannot write twice"

  let make () =
    let p = {state = Pending; handlers = []} in
    p, p

  let return x =
    {state = Resolved x; handlers = []}

  let state p = p.state

  (** requires: [st] may not be [Pending] *)
  let resolve_or_reject (r : 'a resolver) (st : 'a state) =
    assert (st <> Pending);
    let handlers = r.handlers in
    r.handlers <- [];
    write_once r st;
    List.iter (fun f -> f st) handlers

  let reject r x =
    resolve_or_reject r (Rejected x)

  let resolve r x =
    resolve_or_reject r (Resolved x)

  let handler (resolver : 'a resolver) : 'a handler
    = function
      | Pending -> failwith "handler RI violated"
      | Rejected exc -> reject resolver exc
      | Resolved x -> resolve resolver x

  let handler_of_callback
      (callback : 'a -> 'b promise)
      (resolver : 'b resolver) : 'a handler
    = function
      | Pending -> failwith "handler RI violated"
      | Rejected exc -> reject resolver exc
      | Resolved x ->
        let promise = callback x in
        match promise.state with
        | Resolved y -> resolve resolver y
        | Rejected exc -> reject resolver exc
        | Pending -> enqueue (handler resolver) promise

  let (>>=)
      (input_promise : 'a promise)
      (callback : 'a -> 'b promise) : 'b promise
    =
    match input_promise.state with
    | Resolved x -> callback x
    | Rejected exc -> {state = Rejected exc; handlers = []}
    | Pending ->
      let output_promise, output_resolver = make () in
      enqueue (handler_of_callback callback output_resolver) input_promise;
      output_promise
end
module type PROMISE =
  sig
    type 'a state = Pending | Resolved of 'a | Rejected of exn
    type 'a promise
    type 'a resolver
    val make : unit -> 'a promise * 'a resolver
    val return : 'a -> 'a promise
    val state : 'a promise -> 'a state
    val resolve : 'a resolver -> 'a -> unit
    val reject : 'a resolver -> exn -> unit
    val ( >>= ) : 'a promise -> ('a -> 'b promise) -> 'b promise
  end
module Promise : PROMISE

8.7. Monads 8.7. 单子 ¶

A monad is more of a design pattern than a data structure. That is, there are many data structures that, if you look at them in the right way, turn out to be monads.
monad 与其说是一种数据结构,不如说是一种设计模式。也就是说,有许多数据结构,如果你以正确的方式看待它们,就会发现它们是单子。

The name “monad” comes from the mathematical field of category theory, which studies abstractions of mathematical structures. If you ever take a PhD level class on programming language theory, you will likely encounter that idea in more detail. Here, though, we will omit most of the mathematical theory and concentrate on code.
“monad” 这个名字来自范畴论的数学领域,它研究数学结构的抽象。如果您曾经参加过编程语言理论的博士课程,您可能会更详细地遇到这个想法。不过,在这里,我们将省略大部分数学理论并专注于代码。

Monads became popular in the programming world through their use in Haskell, a functional programming language that is even more pure than OCaml—that is, Haskell avoids side effects and imperative features even more than OCaml. But no practical language can do without side effects. After all, printing to the screen is a side effect. So Haskell set out to control the use of side effects through the monad design pattern. Since then, monads have become recognized as useful in other functional programming languages, and are even starting to appear in imperative languages.
Monad 通过在 Haskell 中的使用而在编程世界中流行起来,Haskell 是一种比 OCaml 更纯粹的函数式编程语言,也就是说,Haskell 比 OCaml 更能避免副作用和命令式特征。但任何实用语言都不可能没有副作用。毕竟,打印到屏幕是一个副作用。因此 Haskell 开始通过 monad 设计模式来控制副作用的使用。从那时起,monad 被认为在其他函数式编程语言中很有用,甚至开始出现在命令式语言中。

Monads are used to model computations. Think of a computation as being like a function, which maps an input to an output, but as also doing “something more.” The something more is an effect that the function has as a result of being computed. For example, the effect might involve printing to the screen. Monads provide an abstraction of effects, and help to make sure that effects happen in a controlled order.
Monad 用于模拟计算。将计算视为一个函数,它将输入映射到输出,但也做“更多事情”。更重要的是该函数作为计算结果而产生的效果。例如,效果可能涉及打印到屏幕。 Monad 提供了效果的抽象,并有助于确保效果以受控的顺序发生。

8.7.1. The Monad Signature
8.7.1. Monad 签名 ¶

For our purposes, a monad is a structure that satisfies two properties. First, it must match the following signature:
就我们的目的而言,单子是满足两个属性的结构。首先,它必须匹配以下签名:

module type Monad = sig
  type 'a t
  val return : 'a -> 'a t
  val bind : 'a t -> ('a -> 'b t) -> 'b t
end
module type Monad =
  sig
    type 'a t
    val return : 'a -> 'a t
    val bind : 'a t -> ('a -> 'b t) -> 'b t
  end

Second, a monad must obey what are called the monad laws. We will return to those much later, after we have studied the return and bind operations.
其次,单子必须遵守所谓的单子约束。在我们研究了 returnbind 操作之后,我们稍后会回到这些内容。

Think of a monad as being like a box that contains some value. The value has type 'a, and the box that contains it is of type 'a t. We have previously used a similar box metaphor for both options and promises. That was no accident: options and promises are both examples of monads, as we will see in detail, below.
将单子视为一个包含某些值的盒子。该值的类型为 'a ,包含该值的框的类型为 'a t 。我们之前已经对选项和承诺使用了类似的盒子隐喻。这并非偶然:选项和承诺都是 monad 的例子,我们将在下面详细介绍。

Return. The return operation metaphorically puts a value into a box. You can see that in its type: the input is of type 'a, and the output is of type 'a t.
返回return 操作隐喻地将一个值放入一个框中。您可以在其类型中看到:输入的类型为 'a ,输出的类型为 'a t

In terms of computations, return is intended to have some kind of trivial effect. For example, if the monad represents computations whose side effect is printing to the screen, the trivial effect would be to not print anything.
在计算方面, return 旨在产生某种轻微的作用。例如,如果 monad 表示其副作用是打印到屏幕上的计算,那么最简单的效果就是不打印任何内容。

Bind. The bind operation metaphorically takes as input:
绑定bind 操作隐喻地将其作为输入:

  • a boxed value, which has type 'a t, and
    一个装箱值,其类型为 'a t ,以及

  • a function that itself takes an unboxed value of type 'a as input and returns a boxed value of type 'b t as output.
    一个函数,它本身将 'a 类型的未装箱值作为输入,并返回 'b t 类型的装箱值作为输出。

The bind applies its second argument to the first. That requires taking the 'a value out of its box, applying the function to it, and returning the result.
bind 将其第二个参数应用于第一个参数。这需要从盒子中取出 'a 值,对其应用函数,然后返回结果。

In terms of computations, bind is intended to sequence effects one after another. Continuing the running example of printing, sequencing would mean first printing one string, then another, and bind would be making sure that the printing happens in the correct order.
在计算方面, bind 旨在将效果依次排序。继续打印的运行示例,排序意味着首先打印一个字符串,然后打印另一个字符串,并且 bind 将确保打印以正确的顺序进行。

The usual notation for bind is as an infix operator written >>= and still pronounced “bind”. So let’s revise our signature for monads:
bind 的通常表示法是写为 >>= 的中缀运算符,并且仍然发音为“bind”。因此,让我们修改 monad 的签名:

module type Monad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
end
module type Monad =
  sig
    type 'a t
    val return : 'a -> 'a t
    val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
  end

All of the above is likely to feel very abstract upon first reading. It will help to see some concrete examples of monads. Once you understand several >>= and return operations, the design pattern itself should make more sense.
第一次阅读时,以上所有内容可能会感觉非常抽象。查看 monad 的一些具体示例将会有所帮助。一旦理解了几个 >>=return 操作,设计模式本身就应该更有意义。

So the next few sections look at several different examples of code in which monads can be discovered. Because monads are a design pattern, they aren’t always obvious; it can take some study to tease out where the monad operations are being used.
因此,接下来的几节将介绍几个可以发现 monad 的不同代码示例。因为 monad 是一种设计模式,所以它们并不总是显而易见的;可能需要一些研究才能弄清楚 monad 操作的用途。

8.7.2. The Maybe Monad
8.7.2. 可能单子 ¶

As we’ve seen before, sometimes functions are partial: there is no good output they can produce for some inputs. For example, the function max_list : int list -> int doesn’t necessarily have a good output value to return for the empty list. One possibility is to raise an exception. Another possibility is to change the return type to be int option, and use None to represent the function’s inability to produce an output. In other words, maybe the function produces an output, or maybe it is unable to do so hence returns None.
正如我们之前所看到的,有时函数是不完整的:它们无法为某些输入产生良好的输出。例如,函数 max_list : int list -> int 不一定能够为空列表返回良好的输出值。一种可能性是提出例外。另一种可能性是将返回类型更改为 int option ,并使用 None 表示函数无法产生输出。换句话说,该函数可能会产生输出,或者可能无法这样做因而返回 None

As another example, consider the built-in OCaml integer division function ( / ) : int -> int -> int. If its second argument is zero, it raises an exception. Another possibility, though, would be to change its type to be ( / ) : int -> int -> int option, and return None whenever the divisor is zero.
作为另一个示例,请考虑内置的 OCaml 整数除法函数 ( / ) : int -> int -> int 。如果其第二个参数为零,则会引发异常。不过,另一种可能性是将其类型更改为 ( / ) : int -> int -> int option ,并在除数为零时返回 None

Both of those examples involved changing the output type of a partial function to be an option, thus making the function total. That’s a nice way to program, until you start trying to combine many functions together. For example, because all the integer operations—addition, subtraction, division, multiplication, negation, etc.—expect an int (or two) as input, you can form large expressions out of them. But as soon as you change the output type of division to be an option, you lose that compositionality.
这两个示例都涉及将部分函数的输出类型更改为选项,从而使函数成为完整的。这是一种很好的编程方式,直到您开始尝试将许多功能组合在一起。例如,由于所有整数运算(加法、减法、除法、乘法、求反等)都需要一个 int (或两个)作为输入,因此您可以用它们构成大型表达式。但是,一旦您将除法的输出类型更改为一个选项,您就会失去这种组合性。

Here’s some code to make that idea concrete:
下面是一些使这个想法具体化的代码:

(* works fine *)
let x = 1 + (4 / 2)
val x : int = 3
let div (x:int) (y:int) : int option =
  if y = 0 then None else Some (x / y)

let ( / ) = div

(* won't type check *)
let x = 1 + (4 / 2)
val div : int -> int -> int option = <fun>
val ( / ) : int -> int -> int option = <fun>
File "[4]", line 7, characters 12-19:
7 | let x = 1 + (4 / 2)
                ^^^^^^^
Error: This expression has type int option
       but an expression was expected of type int

The problem is that we can’t add an int to an int option: the addition operator expects its second input to be of type int, but the new division operator returns a value of type int option.
问题是我们无法将 int 添加到 int option :加法运算符期望其第二个输入的类型为 int ,但新的除法运算符返回 int option 类型的值。

One possibility would be to re-code all the existing operators to accept int option as input. For example,
一种可能性是重新编码所有现有运算符以接受 int option 作为输入。例如,

let plus_opt (x:int option) (y:int option) : int option =
  match x,y with
  | None, _ | _, None -> None
  | Some a, Some b -> Some (Stdlib.( + ) a b)

let ( + ) = plus_opt

let minus_opt (x:int option) (y:int option) : int option =
  match x,y with
  | None, _ | _, None -> None
  | Some a, Some b -> Some (Stdlib.( - ) a b)

let ( - ) = minus_opt

let mult_opt (x:int option) (y:int option) : int option =
  match x,y with
  | None, _ | _, None -> None
  | Some a, Some b -> Some (Stdlib.( * ) a b)

let ( * ) = mult_opt

let div_opt (x:int option) (y:int option) : int option =
  match x,y with
  | None, _ | _, None -> None
  | Some a, Some b ->
    if b=0 then None else Some (Stdlib.( / ) a b)

let ( / ) = div_opt
val plus_opt : int option -> int option -> int option = <fun>
val ( + ) : int option -> int option -> int option = <fun>
val minus_opt : int option -> int option -> int option = <fun>
val ( - ) : int option -> int option -> int option = <fun>
val mult_opt : int option -> int option -> int option = <fun>
val ( * ) : int option -> int option -> int option = <fun>
val div_opt : int option -> int option -> int option = <fun>
val ( / ) : int option -> int option -> int option = <fun>
(* does type check *)
let x = Some 1 + (Some 4 / Some 2)
val x : int option = Some 3

But that’s a tremendous amount of code duplication. We ought to apply the Abstraction Principle and deduplicate. Three of the four operators can be handled by abstracting a function that just does some pattern matching to propagate None:
但这是大量的代码重复。我们应该应用抽象原则并进行重复数据删除。四个运算符中的三个可以通过抽象一个函数来处理,该函数仅执行一些模式匹配来传播 None

let propagate_none (op : int -> int -> int) (x : int option) (y : int option) =
  match x, y with
  | None, _ | _, None -> None
  | Some a, Some b -> Some (op a b)

let ( + ) = propagate_none Stdlib.( + )
let ( - ) = propagate_none Stdlib.( - )
let ( * ) = propagate_none Stdlib.( * )
val propagate_none :
  (int -> int -> int) -> int option -> int option -> int option = <fun>
val ( + ) : int option -> int option -> int option = <fun>
val ( - ) : int option -> int option -> int option = <fun>
val ( * ) : int option -> int option -> int option = <fun>

Unfortunately, division is harder to deduplicate. We can’t just pass Stdlib.( / ) to propagate_none, because neither of those functions will check to see whether the divisor is zero. It would be nice if we could pass our function div : int -> int -> int option to propagate_none, but the return type of div makes that impossible.
不幸的是,除法更难消除重复。我们不能只将 Stdlib.( / ) 传递给 propagate_none ,因为这两个函数都不会检查除数是否为零。如果我们可以将函数 div : int -> int -> int option 传递给 propagate_none ,那就太好了,但是 div 的返回类型使得这变得不可能。

So, let’s rewrite propagate_none to accept an operator of the same type as div, which makes it easy to implement division:
因此,让我们重写 propagate_none 以接受与 div 相同类型的运算符,这样可以轻松实现除法:

let propagate_none
  (op : int -> int -> int option) (x : int option) (y : int option)
=
  match x, y with
  | None, _ | _, None -> None
  | Some a, Some b -> op a b

let ( / ) = propagate_none div
val propagate_none :
  (int -> int -> int option) -> int option -> int option -> int option =
  <fun>
val ( / ) : int option -> int option -> int option = <fun>

Implementing the other three operations requires a little more work, because their return type is int not int option. We need to wrap their return value with Some:
实现其他三个操作需要更多的工作,因为它们的返回类型是 int 而不是 int option 。我们需要用 Some 包装它们的返回值:

let wrap_output (op : int -> int -> int) (x : int) (y : int) : int option =
  Some (op x y)

let ( + ) = propagate_none (wrap_output Stdlib.( + ))
let ( - ) = propagate_none (wrap_output Stdlib.( - ))
let ( * ) = propagate_none (wrap_output Stdlib.( * ))
val wrap_output : (int -> int -> int) -> int -> int -> int option = <fun>
val ( + ) : int option -> int option -> int option = <fun>
val ( - ) : int option -> int option -> int option = <fun>
val ( * ) : int option -> int option -> int option = <fun>

Finally, we could re-implement div to use wrap_output:
最后,我们可以重新实现 div 以使用 wrap_output

let div (x : int) (y : int) : int option =
  if y = 0 then None else wrap_output Stdlib.( / ) x y

let ( / ) = propagate_none div
val div : int -> int -> int option = <fun>
val ( / ) : int option -> int option -> int option = <fun>

Where’s the Monad? The work we just did was to take functions on integers and transform them into functions on values that maybe are integers, but maybe are not—that is, values that are either Some i where i is an integer, or are None. We can think of these “upgraded” functions as computations that may have the effect of producing nothing. They produce metaphorical boxes, and those boxes may be full of something, or contain nothing.
可单子在哪里呢?我们刚刚所做的工作是采用整数函数并将它们转换为值的函数,这些值可能是整数,但也可能不是,即 Some ii 的值是一个整数,或者是 None 。我们可以将这些“升级”的函数视为可能不会产生任何结果的计算。他们制造出隐喻的盒子,这些盒子可能装满了东西,也可能什么也没有。

There were two fundamental ideas in the code we just wrote, which correspond to the monad operations of return and bind.
我们刚刚编写的代码中有两个基本思想,分别对应于 returnbind 的单子操作。

The first (which admittedly seems trivial) was upgrading a value from int to int option by wrapping it with Some. That’s what the body of wrap_output does. We could expose that idea even more clearly by defining the following function:
第一个(诚然看起来微不足道)是通过用 Some 包装将值从 int 升级到 int option 。这就是 wrap_output 主体的作用。我们可以通过定义以下函数来更清楚地揭示这个想法:

let return (x : int) : int option = Some x
val return : int -> int option = <fun>

This function has the trivial effect of putting a value into the metaphorical box.
这个函数具备细微的作用,就是将一个值放入隐喻框中。

The second idea was factoring out code to handle all the pattern matching against None. We had to upgrade functions whose inputs were of type int to instead accept inputs of type int option. Here’s that idea expressed as its own function:
第二个想法是分解代码来处理与 None 匹配的所有模式。我们必须升级输入为 int 类型的函数,以接受 int option 类型的输入。这是用它自己的函数表达的想法:

let bind (x : int option) (op : int -> int option) : int option =
  match x with
  | None -> None
  | Some a -> op a

let ( >>= ) = bind
val bind : int option -> (int -> int option) -> int option = <fun>
val ( >>= ) : int option -> (int -> int option) -> int option = <fun>

The bind function can be understood as doing the core work of upgrading op from a function that accepts an int as input to a function that accepts an int option as input. In fact, we could even write a function that does that upgrading for us using bind:
bind 函数可以理解为执行将 op 从接受 int 作为输入的函数升级到接受 int option 作为输入。事实上,我们甚至可以编写一个使用 bind 为我们进行升级的函数:

let upgrade : (int -> int option) -> (int option -> int option) =
  fun (op : int -> int option) (x : int option) -> (x >>= op)
val upgrade : (int -> int option) -> int option -> int option = <fun>

All those type annotations are intended to help the reader understand the function. Of course, it could be written much more simply as:
所有这些类型注释都是为了帮助读者理解该函数。当然,还可以更简单地写成:

let upgrade op x = x >>= op
val upgrade : (int -> int option) -> int option -> int option = <fun>

Using just the return and >>= functions, we could re-implement the arithmetic operations from above:
仅使用 return>>= 函数,我们可以重新实现上面的算术运算:

let ( + ) (x : int option) (y : int option) : int option =
  x >>= fun a ->
  y >>= fun b ->
  return (Stdlib.( + ) a b)

let ( - ) (x : int option) (y : int option) : int option =
  x >>= fun a ->
  y >>= fun b ->
  return (Stdlib.( - ) a b)

let ( * ) (x : int option) (y : int option) : int option =
  x >>= fun a ->
  y >>= fun b ->
  return (Stdlib.( * ) a b)

let ( / ) (x : int option) (y : int option) : int option =
  x >>= fun a ->
  y >>= fun b ->
  if b = 0 then None else return (Stdlib.( / ) a b)
val ( + ) : int option -> int option -> int option = <fun>
val ( - ) : int option -> int option -> int option = <fun>
val ( * ) : int option -> int option -> int option = <fun>
val ( / ) : int option -> int option -> int option = <fun>

Recall, from our discussion of the bind operator in Lwt, that the syntax above should be parsed by your eye as
回想一下,根据我们对 Lwt 中绑定运算符的讨论,上面的语法应该由您的眼睛解析为

  • take x and extract from it the value a,
    获取 x 并从中提取值 a

  • then take y and extract from it b,
    然后取出 y 并从中提取 b

  • then use a and b to construct a return value.
    然后使用 ab 构造返回值。

Of course, there’s still a fair amount of duplication going on there. We can de-duplicate by using the same techniques as we did before:
当然,那里仍然存在大量重复。我们可以使用与之前相同的技术来消除重复:

let upgrade_binary op x y =
  x >>= fun a ->
  y >>= fun b ->
  op a b

let return_binary op x y = return (op x y)

let ( + ) = upgrade_binary (return_binary Stdlib.( + ))
let ( - ) = upgrade_binary (return_binary Stdlib.( - ))
let ( * ) = upgrade_binary (return_binary Stdlib.( * ))
let ( / ) = upgrade_binary div
val upgrade_binary :
  (int -> int -> int option) -> int option -> int option -> int option =
  <fun>
val return_binary : ('a -> 'b -> int) -> 'a -> 'b -> int option = <fun>
val ( + ) : int option -> int option -> int option = <fun>
val ( - ) : int option -> int option -> int option = <fun>
val ( * ) : int option -> int option -> int option = <fun>
val ( / ) : int option -> int option -> int option = <fun>

The Maybe Monad. The monad we just discovered goes by several names: the maybe monad (as in, “maybe there’s a value, maybe not”), the error monad (as in, “either there’s a value or an error”, and error is represented by None—though some authors would want an error monad to be able to represent multiple kinds of errors rather than just collapse them all to None), and the option monad (which is obvious).
可能单子。我们刚刚发现的 monad 有几个名字: Maybe monad(比如,“也许有一个值,也许没有”),error monad(比如,“要么有一个值,要么有一个错误”,错误表示为 None ——尽管有些作者希望错误 monad 能够表示多种错误,而不是仅仅将它们全部折叠到 None ),以及选项 monad (这是显而易见的) 。

Here’s an implementation of the monad signature for the maybe monad:
这是 Maybe monad 的 monad 签名的实现:

module Maybe : Monad = struct
  type 'a t = 'a option

  let return x = Some x

  let (>>=) m f =
    match m with
    | None -> None
    | Some x -> f x
end
module Maybe : Monad

These are the same implementations of return and >>= as we invented above, but without the type annotations to force them to work only on integers. Indeed, we never needed those annotations; they just helped make the code above a little clearer.
这些与我们上面发明的 return>>= 的实现相同,但没有类型注释来强制它们仅适用于整数。事实上,我们从来不需要这些注释;我们不需要这些注释。他们只是帮助使上面的代码更清晰一些。

In practice the return function here is quite trivial and not really necessary. But the >>= operator can be used to replace a lot of boilerplate pattern matching, as we saw in the final implementation of the arithmetic operators above. There’s just a single pattern match, which is inside of >>=. Compare that to the original implementations of plus_opt, etc., which had many pattern matches.
实际上,这里的 return 函数非常简单,并不是真正必要的。但是 >>= 运算符可以用来替换许多样板模式匹配,正如我们在上面算术运算符的最终实现中看到的那样。只有一个模式匹配,位于 >>= 内部。将其与 plus_opt 等的原始实现进行比较,这些实现有许多模式匹配。

The result is we get code that (once you understand how to read the bind operator) is easier to read and easier to maintain.
结果是我们得到的代码(一旦您了解如何阅读绑定运算符)更易于阅读和维护。

Now that we’re done playing with integer operators, we should restore their original meaning for the rest of this file:
现在我们已经完成了整数运算符的操作,我们应该恢复该文件其余部分的原始含义:

let ( + ) = Stdlib.( + )
let ( - ) = Stdlib.( - )
let ( * ) = Stdlib.( * )
let ( / ) = Stdlib.( / )
val ( + ) : int -> int -> int = <fun>
val ( - ) : int -> int -> int = <fun>
val ( * ) : int -> int -> int = <fun>
val ( / ) : int -> int -> int = <fun>

8.7.3. Example: The Writer Monad
8.7.3. 示例:写入器单子 ¶

When trying to diagnose faults in a system, it’s often the case that a log of what functions have been called, as well as what their inputs and outputs were, would be helpful.
当尝试诊断系统中的故障时,通常情况下,调用哪些函数及其输入和输出的日志会很有帮助。

Imagine that we had two functions we wanted to debug, both of type int -> int. For example:
想象一下,我们有两个要调试的函数,都是 int -> int 类型。例如:

let inc x = x + 1
let dec x = x - 1
val inc : int -> int = <fun>
val dec : int -> int = <fun>

(Ok, those are really simple functions; we probably don’t need any help debugging them. But imagine they compute something far more complicated, like encryptions or decryptions of integers.)
(好吧,这些都是非常简单的函数;我们可能不需要任何帮助来调试它们。但想象一下它们计算更复杂的东西,比如整数的加密或解密。)

One way to keep a log of function calls would be to augment each function to return a pair: the integer value the function would normally return, as well as a string containing a log message. For example:
保留函数调用日志的一种方法是增加每个函数以返回一对:函数通常返回的整数值以及包含日志消息的字符串。例如:

let inc_log x = (x + 1, Printf.sprintf "Called inc on %i; " x)
let dec_log x = (x - 1, Printf.sprintf "Called dec on %i; " x)
val inc_log : int -> int * string = <fun>
val dec_log : int -> int * string = <fun>

But that changes the return type of both functions, which makes it hard to compose the functions. Previously, we could have written code such as
但这会改变两个函数的返回类型,这使得组合函数变得困难。以前,我们可以编写如下代码

let id x = dec (inc x)
val id : int -> int = <fun>

or even better 甚至更好

let id x = x |> inc |> dec
val id : int -> int = <fun>

or even better still, using the composition operator >>,
或者更好的是,使用组合运算符 >>

let ( >> ) f g x = x |> f |> g
let id = inc >> dec
val ( >> ) : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c = <fun>
val id : int -> int = <fun>

and that would have worked just fine. But trying to do the same thing with the loggable versions of the functions produces a type-checking error:
这样就可以了。但是尝试对函数的可记录版本执行相同的操作会产生类型检查错误:

let id = inc_log >> dec_log
File "[24]", line 1, characters 20-27:
1 | let id = inc_log >> dec_log
                        ^^^^^^^
Error: This expression has type int -> int * string
       but an expression was expected of type int * string -> 'a
       Type int is not compatible with type int * string 

That’s because inc_log x would be a pair, but dec_log expects simply an integer as input.
这是因为 inc_log x 是一对,但 dec_log 只需要一个整数作为输入。

We could code up an upgraded version of dec_log that is able to take a pair as input:
我们可以编写 dec_log 的升级版本,它能够将一对作为输入:

let dec_log_upgraded (x, s) =
  (x - 1, Printf.sprintf "%s; Called dec on %i; " s x)

let id x = x |> inc_log |> dec_log_upgraded
val dec_log_upgraded : int * string -> int * string = <fun>
val id : int -> int * string = <fun>

That works fine, but we also will need to code up a similar upgraded version of f_log if we ever want to call them in reverse order, e.g., let id = dec_log >> inc_log. So we have to write:
这工作得很好,但如果我们想以相反的顺序调用它们,我们还需要编写一个类似的升级版本 f_log ,例如 let id = dec_log >> inc_log 。所以我们必须写:

let inc_log_upgraded (x, s) =
  (x + 1, Printf.sprintf "%s; Called inc on %i; " s x)

let id = dec_log >> inc_log_upgraded
val inc_log_upgraded : int * string -> int * string = <fun>
val id : int -> int * string = <fun>

And at this point we’ve duplicated far too much code. The implementations of inc and dec are duplicated inside both inc_log and dec_log, as well as inside both upgraded versions of the functions. And both the upgrades duplicate the code for concatenating log messages together. The more functions we want to make loggable, the worse this duplication is going to become!
此时我们已经重复了太多的代码。 incdec 的实现在 inc_logdec_log 以及函数的两个升级版本中重复。这两个升级都重复了将日志消息连接在一起的代码。我们想要记录的函数越多,这种重复就会变得越糟糕!

So, let’s start over, and factor out a couple helper functions. The first helper calls a function and produces a log message:
因此,让我们重新开始,并分解出几个辅助函数。第一个助手调用一个函数并生成一条日志消息:

let log (name : string) (f : int -> int) : int -> int * string =
  fun x -> (f x, Printf.sprintf "Called %s on %i; " name x)
val log : string -> (int -> int) -> int -> int * string = <fun>

The second helper produces a logging function of type 'a * string -> 'b * string out of a non-loggable function:
第二个助手从不可记录的函数中生成 'a * string -> 'b * string 类型的记录函数:

let loggable (name : string) (f : int -> int) : int * string -> int * string =
  fun (x, s1) ->
    let (y, s2) = log name f x in
    (y, s1 ^ s2)
val loggable : string -> (int -> int) -> int * string -> int * string = <fun>

Using those helpers, we can implement the logging versions of our functions without any duplication of code involving pairs or pattern matching or string concatenation:
使用这些助手,我们可以实现函数的日志记录版本,而无需涉及对或模式匹配或字符串连接的任何重复代码:

let inc' : int * string -> int * string =
  loggable "inc" inc

let dec' : int * string -> int * string =
  loggable "dec" dec

let id' : int * string -> int * string =
  inc' >> dec'
val inc' : int * string -> int * string = <fun>
val dec' : int * string -> int * string = <fun>
val id' : int * string -> int * string = <fun>

Here’s an example usage:
这是一个用法示例:

id' (5, "")
- : int * string = (5, "Called inc on 5; Called dec on 6; ")

Notice how it’s inconvenient to call our loggable functions on integers, since we have to pair the integer with a string. So let’s write one more function to help with that by pairing an integer with the empty log:
请注意,在整数上调用可记录函数是多么不方便,因为我们必须将整数与字符串配对。因此,让我们再编写一个函数来帮助实现这一点,将一个整数与空日志配对:

let e x = (x, "")
val e : 'a -> 'a * string = <fun>

And now we can write id' (e 5) instead of id' (5, "").
现在我们可以写 id' (e 5) 而不是 id' (5, "")

Where’s the Monad? The work we just did was to take functions on integers and transform them into functions on integers paired with log messages. We can think of these “upgraded” functions as computations that log. They produce metaphorical boxes, and those boxes contain function outputs as well as log messages.
单子在哪儿?我们刚刚所做的工作是将整数函数转换为与日志消息配对的整数函数。我们可以将这些“升级”的函数视为记录的计算。它们产生隐喻框,这些框包含函数输出以及日志消息。

There were two fundamental ideas in the code we just wrote, which correspond to the monad operations of return and bind.
我们刚刚编写的代码中有两个基本思想,分别对应于 returnbind 的 monad 操作。

The first was upgrading a value from int to int * string by pairing it with the empty string. That’s what e does. We could rename it return:
第一个是将值与空字符串配对,从 int 升级到 int * string 。这就是 e 的作用。我们可以将其重命名为 return

let return (x : int) : int * string = (x, "")
val return : int -> int * string = <fun>

This function has the trivial effect of putting a value into the metaphorical box along with the empty log message.
该函数具有将值与空日志消息一起放入隐喻框中的微不足道的效果。

The second idea was factoring out code to handle pattern matching against pairs and string concatenation. Here’s that idea expressed as its own function:
第二个想法是分解代码来处理对和字符串连接的模式匹配。这是用它自己的函数表达的想法:

let ( >>= ) (m : int * string) (f : int -> int * string) : int * string =
  let (x, s1) = m in
  let (y, s2) = f x in
  (y, s1 ^ s2)
val ( >>= ) : int * string -> (int -> int * string) -> int * string = <fun>

Using >>=, we can re-implement loggable, such that no pairs or pattern matching are ever used in its body:
使用 >>= ,我们可以重新实现 loggable ,这样它的主体中就不会使用任何对或模式匹配:

let loggable (name : string) (f : int -> int) : int * string -> int * string =
  fun m ->
    m >>= fun x ->
    log name f x
val loggable : string -> (int -> int) -> int * string -> int * string = <fun>

The Writer Monad. The monad we just discovered is usually called the writer monad (as in, “additionally writing to a log or string”). Here’s an implementation of the monad signature for it:
写入器单子。我们刚刚发现的 monad 通常称为 writer monad(如“另外写入日志或字符串”)。这是 monad 签名的实现:

module Writer : Monad = struct
  type 'a t = 'a * string

  let return x = (x, "")

  let ( >>= ) m f =
    let (x, s1) = m in
    let (y, s2) = f x in
    (y, s1 ^ s2)
end
module Writer : Monad

As we saw with the maybe monad, these are the same implementations of return and >>= as we invented above, but without the type annotations to force them to work only on integers. Indeed, we never needed those annotations; they just helped make the code above a little clearer.
正如我们在 Maybe monad 中看到的,这些与我们上面发明的 return>>= 的实现相同,但没有类型注释来强制它们仅适用于整数。事实上,我们从来不需要这些注释;我们不需要这些注释。他们只是帮助使上面的代码更清晰一些。

It’s debatable which version of loggable is easier to read. Certainly you need to be comfortable with the monadic style of programming to appreciate the version of it that uses >>=. But if you were developing a much larger code base (i.e., with more functions involving paired strings than just loggable), using the >>= operator is likely to be a good choice: it means the code you write can concentrate on the 'a in the type 'a Writer.t instead of on the strings. In other words, the writer monad will take care of the strings for you, as long as you use return and >>=.
哪个版本的 loggable 更容易阅读是有争议的。当然,您需要熟悉一元编程风格才能欣赏使用 >>= 的版本。但是,如果您正在开发更大的代码库(即,有更多涉及配对字符串的函数而不仅仅是 loggable ),那么使用 >>= 运算符可能是一个不错的选择:这意味着您编写的代码可以集中在类型 'a Writer.t 中的 'a 而不是字符串上。换句话说,只要您使用 return>>= ,编写器 monad 就会为您处理字符串。

8.7.4. Example: The Lwt Monad
8.7.4. 示例:Lwt Monad ¶

By now, it’s probably obvious that the Lwt promises library that we discussed is also a monad. The type 'a Lwt.t of promises has a return and bind operation of the right types to be a monad:
到目前为止,很明显我们讨论的 Lwt Promise 库也是一个 monad。 Promise 的类型 'a Lwt.t 具有成为 monad 的正确类型的 returnbind 操作:

val return : 'a -> 'a t
val bind : 'a t -> ('a -> 'b t) -> 'b t

And Lwt.Infix.( >>= ) is a synonym for Lwt.bind, so the library does provide an infix bind operator.
Lwt.Infix.( >>= )Lwt.bind 的同义词,因此该库确实提供了中缀绑定运算符。

Now we start to see some of the great power of the monad design pattern. The implementation of 'a t and return that we saw before involves creating references, but those references are completely hidden behind the monadic interface. Moreover, we know that bind involves registering callbacks, but that functionality (which as you might imagine involves maintaining collections of callbacks) is entirely encapsulated.
现在我们开始看到 monad 设计模式的一些强大功能。我们之前看到的 'a treturn 的实现涉及创建引用,但这些引用完全隐藏在单子接口后面。此外,我们知道 bind 涉及注册回调,但该功能(正如您可能想象的那样涉及维护回调集合)是完全封装的。

Metaphorically, as we discussed before, the box involved here is one that starts out empty but eventually will be filled with a value of type 'a. The “something more” in these computations is that values are being produced asynchronously, rather than immediately.
打个比方,正如我们之前讨论的,这里涉及的盒子一开始是空的,但最终会被 'a 类型的值填充。这些计算中的“更多内容”即是:值是异步生成的、而不是立即生成的。

8.7.5. Monad Laws 8.7.5. 单子约束 ¶

Every data structure has not just a signature, but some expected behavior. For example, a stack has a push and a pop operation, and we expect those operations to satisfy certain algebraic laws. We saw those for stacks when we studied equational specification:
每个数据结构不仅有一个签名,还有一些预期的行为。例如,堆栈有入栈和出栈操作,我们期望这些操作满足某些代数定律。当我们研究等式规范时,我们看到了堆栈的那些:

  • peek (push x s) = x

  • pop (push x empty) = empty

  • etc.

A monad, though, is not just a single data structure. It’s a design pattern for data structures. So it’s impossible to write specifications of return and >>= for monads in general: the specifications would need to discuss the particular monad, like the writer monad or the Lwt monad.
然而,单子不仅仅是一个单一的数据结构。它是数据结构的设计模式。因此,一般来说不可能为 monad 编写 return>>= 规范:规范需要讨论特定的 monad,例如 writer monad 或 Lwt monad。

On the other hand, it turns out that we can write down some laws that ought to hold of any monad. The reason for that goes back to one of the intuitions we gave about monads, namely, that they represent computations that have effects. Consider Lwt, for example. We might register a callback C on promise X with bind. That produces a new promise Y, on which we could register another callback D. We expect a sequential ordering on those callbacks: C must run before D, because Y cannot be resolved before X.
另一方面,事实证明我们可以写下一些适用于任何单子的规定。其原因可以追溯到我们对单子的直觉之一,即它们代表具有效果的计算。例如,考虑 Lwt。我们可以使用 bind 在 Promise X 上注册回调 C。这会产生一个新的 Promise Y,我们可以在其中注册另一个回调 D。我们期望这些回调按顺序排列:C 必须在 D 之前运行,因为 Y 无法在 X 之前解析。

That notion of sequential order is part of what the monad laws stipulate. We will state those laws below. But first, let’s pause to consider sequential order in imperative languages.
顺序的概念是单子法则规定的一部分。我们将在下面阐述这些法律。但首先,让我们停下来考虑一下命令式语言中的顺序。

*Sequential Order. In languages like Java and C, there is a semicolon that imposes a sequential order on statements, e.g.:
*顺序。在 Java 和 C 等语言中,有一个分号对语句施加顺序,例如:

System.out.println(x);
x++;
System.out.println(x);

First x is printed, then incremented, then printed again. The effects that those statements have must occur in that sequential order.
首先打印 x ,然后递增,然后再次打印。这些语句所产生的效果必须按顺序发生。

Let’s imagine a hypothetical statement that causes no effect whatsoever. For example, assert true causes nothing to happen in Java. (Some compilers will completely ignore it and not even produce bytecode for it.) In most assembly languages, there is likewise a “no op” instruction whose mnemonic is usually NOP that also causes nothing to happen. (Technically, some clock cycles would elapse. But there wouldn’t be any changes to registers or memory.) In the theory of programming languages, statements like this are usually called skip, as in, “skip over me because I don’t do anything interesting.”
让我们想象一个不会产生任何影响的假设陈述。例如, assert true 在 Java 中不会发生任何事情。 (有些编译器会完全忽略它,甚至不为其生成字节码。)在大多数汇编语言中,同样有一条“no op”指令,其助记符通常为 NOP ,它也不会导致任何事情发生。 (从技术上讲,一些时钟周期会过去。但寄存器或内存不会有任何变化。)在编程语言理论中,这样的语句通常称为 skip ,如“跳过”我,因为我不做任何有趣的事情。”

Here are two laws that should hold of skip and semicolon:
以下是 skip 和分号应遵循的两条定律:

  • skip; s; should behave the same as just s;.
    skip; s; 的行为应与 s; 相同。

  • s; skip; should behave the same as just s;.
    s; skip; 的行为应与 s; 相同。

In other words, you can remove any occurrences of skip, because it has no effects. Mathematically, we say that skip is a left identity (the first law) and a right identity (the second law) of semicolon.
换句话说,您可以删除任何出现的 skip ,因为它没有任何效果。从数学上来说,我们说 skip 是分号的左恒等式(第一定律)和右恒等式(第二定律)。

Imperative languages also usually have a way of grouping statements together into blocks. In Java and C, this is usually done with curly braces. Here is a law that should hold of blocks and semicolon:
命令式语言通常还具有将语句分组为块的方法。在 Java 和 C 中,这通常是通过花括号完成的。这是一个适用于块和分号的定律:

  • {s1; s2;} s3; should behave the same as s1; {s2; s3;}.
    {s1; s2;} s3; 的行为应与 s1; {s2; s3;} 相同。

In other words, the order is always s1 then s2 then s3, regardless of whether you group the first two statements into a block or the second two into a block. So you could even remove the braces and just write s1; s2; s3;, which is what we normally do anyway. Mathematically, we say that semicolon is associative.
换句话说,顺序始终是 s1 然后 s2 然后 s3 ,无论您是将前两个语句分组到一个块中还是将后两个语句分组到一个块中堵塞。所以你甚至可以去掉大括号,只写 s1; s2; s3; ,这就是我们通常所做的。从数学上来说,我们说分号具有结合性。

Sequential Order with the Monad Laws. The three laws above embody exactly the same intuition as the monad laws, which we will now state. The monad laws are just a bit more abstract hence harder to understand at first.
顺序性与单子规定。上述三个定律体现了与我们现在要阐述的单子定律完全相同的直觉。单子定律只是更抽象一点,因此一开始更难理解。

Suppose that we have any monad, which as usual must have the following signature:
假设我们有任何 monad,它通常必须具有以下签名:

module type Monad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
end
module type Monad =
  sig
    type 'a t
    val return : 'a -> 'a t
    val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
  end

The three monad laws are as follows:
三个单子定律如下:

  • Law 1: return x >>= f behaves the same as f x.
    法则 1: return x >>= f 的行为与 f x 相同。

  • Law 2: m >>= return behaves the same as m.
    法则 2: m >>= return 的行为与 m 相同。

  • Law 3: (m >>= f) >>= g behaves the same as m >>= (fun x -> f x >>= g).
    法则 3: (m >>= f) >>= g 的行为与 m >>= (fun x -> f x >>= g) 相同。

Here, “behaves the same as” means that the two expressions will both evaluate to the same value, or they will both go into an infinite loop, or they will both raise the same exception.
这里,“行为相同”意味着两个表达式都将计算出相同的值,或者它们都将进入无限循环,或者它们都将引发相同的异常。

These laws are mathematically saying the same things as the laws for skip, semicolon, and braces that we saw above: return is a left and right identity of >>=, and >>= is associative. Let’s look at each law in more detail.
这些定律在数学上与我们上面看到的 skip 、分号和大括号的定律有相同的含义: return>>= 的左右标识,并且 >>= 是关联的。让我们更详细地了解每条法律。

Law 1 says that having the trivial effect on a value, then binding a function on it, is the same as just calling the function on the value. Consider the maybe monad: return x would be Some x, and >>= f would extract x and apply f to it. Or consider the Lwt monad: return x would be a promise that is already resolved with x, and >>= f would register f as a callback to run on x.
定律 1 规定,对某个值产生微不足道的影响,然后在其上绑定一个函数,与仅对该值调用该函数的效果相同。考虑可能的单子: return x 将是 Some x ,而 >>= f 将提取 x 并将 f 应用于它。或者考虑 Lwt monad: return x 将是一个已经用 x 解决的承诺,并且 >>= f 将注册 f 作为回调在 x 上运行。

Law 2 says that binding on the trivial effect is the same as just not having the effect. Consider the maybe monad: m >>= return would depend upon whether m is Some x or None. In the former case, binding would extract x, and return would just re-wrap it with Some. In the latter case, binding would just return None. Similarly, with Lwt, binding on m would register return as a callback to be run on the contents of m after it is resolved, and return would just take those contents and put them back into an already resolved promise.
定律 2 规定,对微小效果的约束力与不具有效果相同。考虑可能的单子: m >>= return 将取决于 mSome x 还是 None 。在前一种情况下,绑定将提取 x ,而 return 只会用 Some 重新包装它。在后一种情况下,绑定只会返回 None 。类似地,使用 Lwt,绑定 m 会将 return 注册为回调,在解析后对 m 的内容运行,并且 return

Law 3 says that bind sequences effects correctly, but it’s harder to see it in this law than it was in the version above with semicolon and braces. Law 3 would be clearer if we could rewrite it as
定律 3 规定了绑定序列效果正确,但在该定律中比在上面带有分号和大括号的版本中更难看到它。如果我们可以将定律 3 改写为

(m >>= f) >>= g behaves the same as m >>= (f >>= g).
(m >>= f) >>= g 的行为与 m >>= (f >>= g) 相同。

But the problem is that doesn’t type check: f >>= g doesn’t have the right type to be on the right-hand side of >>=. So we have to insert an extra anonymous function fun x -> ... to make the types correct.
但问题是没有进行类型检查: f >>= g 没有正确的类型位于 >>= 的右侧。所以我们必须插入一个额外的匿名函数 fun x -> ... 来使类型正确。

8.7.6. Composition and Monad Laws
8.7.6. 组合与 Monad 定律 ¶

There is another monad operator called compose that can be used to compose monadic functions. For example, suppose you have a monad with type 'a t, and two functions:
还有另一个称为 compose 的 monad 运算符,可用于组合 monadic 函数。例如,假设您有一个类型为 'a t 的 monad 和两个函数:

  • f : 'a -> 'b t

  • g : 'b -> 'c t

The composition of those functions would be
这些函数的组成将是

  • compose f g : 'a -> 'c t

That is, the composition would take a value of type 'a, apply f to it, extract the 'b out of the result, apply g to it, and return that value.
也就是说,组合将采用 'a 类型的值,对其应用 f ,从结果中提取 'b ,应用 g

We can code up compose using >>=; we don’t need to know anything more about the inner workings of the monad:
我们可以使用 >>= 来编写 compose ;我们不需要更多地了解 monad 的内部运作方式:

let compose f g x =
  f x >>= fun y ->
  g y

let ( >=> ) = compose
val compose :
  ('a -> int * string) -> (int -> int * string) -> 'a -> int * string = <fun>
val ( >=> ) :
  ('a -> int * string) -> (int -> int * string) -> 'a -> int * string = <fun>

As the last line suggests, compose can be expressed as infix operator written >=>.
正如最后一行所示, compose 可以表示为中缀运算符 >=>

Returning to our example of the maybe monad with a safe division operator, imagine that we have increment and decrement functions:
回到带有安全除法运算符的 Maybe monad 的示例,假设我们有递增和递减函数:

let inc (x : int) : int option = Some (x + 1)
let dec (x : int) : int option = Some (x - 1)
let ( >>= ) x op =
  match x with
  | None -> None
  | Some a -> op a
val inc : int -> int option = <fun>
val dec : int -> int option = <fun>
val ( >>= ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>

The monadic compose operator would enable us to compose those two into an identity function without having to write any additional code:
单子组合运算符使我们能够将这两个组合成一个恒等函数,而无需编写任何额外的代码:

let ( >=> ) f g x =
  f x >>= fun y ->
  g y

let id : int -> int option = inc >=> dec
val ( >=> ) : ('a -> 'b option) -> ('b -> 'c option) -> 'a -> 'c option =
  <fun>
val id : int -> int option = <fun>

Using the compose operator, there is a much cleaner formulation of the monad laws:
使用 compose 运算符,可以更清晰地表述单子定律:

  • Law 1: return >=> f behaves the same as f.
    法则 1: return >=> f 的行为与 f 相同。

  • Law 2: f >=> return behaves the same as f.
    法则 2: f >=> return 的行为与 f 相同。

  • Law 3: (f >=> g) >=> h behaves the same as f >=> (g >=> h).
    法则 3: (f >=> g) >=> h 的行为与 f >=> (g >=> h) 相同。

In that formulation, it becomes immediately clear that return is a left and right identity, and that composition is associative.
在该表述中,立即可以清楚地看出 return 是左右恒等式,并且该组合是关联的。

8.8. Summary 8.8. 小结 ¶

This chapter has taken a deep dive into some advanced data structures, analysis techniques, and programming patterns. Our goal has been to write correct, efficient, beautiful code. Did we succeed? You can be the judge.
本章深入探讨了一些高级数据结构、分析技术和编程模式。我们的目标是编写正确、高效、美观的代码。我们成功了吗?你可以当法官。

8.8.1. Terms and Concepts
8.8.1. 术语和概念 ¶

  • amortized analysis 摊销分析

  • association list 关联列表

  • associative 联想性的

  • associative array 关联数组

  • asymptotic bound 渐近界

  • asynchronous 异步

  • banker’s method 银行家的方法

  • big oh 大哦

  • bind 绑定

  • binding 捆绑

  • blocking 阻塞

  • brute force 蛮力

  • bucket 

  • caching 缓存

  • callback 打回来

  • chaining 链接

  • channel 渠道

  • collision 碰撞

  • complexity 复杂

  • computations 计算

  • concurrent 同时

  • concurrent composition 并行组合

  • cooperative 合作社

  • credits 学分

  • cycle 循环

  • delayed evaluation 延迟评估

  • deterministic 确定性的

  • dictionary 字典

  • diffusion 扩散

  • direct address table 直接地址表

  • eager 渴望的

  • effects 效果

  • efficiency 效率

  • execution steps 执行步骤

  • exponential time 指数时间

  • force 力量

  • hash function 哈希函数

  • infinite data structure 无限数据结构

  • injective 单射

  • input size 输入尺寸

  • interleaving 交错

  • key

  • latency hiding 延迟隐藏

  • lazy 懒惰的

  • left identity 留下身份

  • load factor 负载系数

  • Lwt monad 通过单子

  • map

  • maybe monad 也许单子

  • memoization 记忆化

  • monads 单子

  • monads laws 单子法则

  • mutable map 可变映射

  • non-blocking 非阻塞

  • nondeterministic 不确定性的

  • parallelism 并行性

  • pending 待办的

  • persistent 执着的

  • physicist’s method 物理学家的方法

  • polynomial time 多项式时间

  • potential energy 势能

  • preemptive 先发制人

  • probing 探测

  • promises 承诺

  • race conditions 竞争条件

  • recursive values 递归值

  • red-black map 红黑地图

  • rejected 拒绝

  • resizing 调整大小

  • resolution loop 分辨率循环

  • resolved 解决

  • resolver 解析器

  • right identity 正确的身份

  • sequential 顺序的

  • sequential composition 顺序组合

  • serialization 序列化

  • set

  • standard input 标准输入

  • standard output 标准输出

  • stream 溪流

  • strict 严格的

  • synchronous 同步

  • threads 线程

  • thunk 重击

  • worst case performance 最坏情况下的性能

  • writer monad 作家单子

8.8.2. Further Reading 8.8.2. 延伸阅读 ¶

  • More OCaml: Algorithms, Methods, and Diversions, chapters 2 and 11, by John Whitington.
    更多 OCaml:算法、方法和转移,第 2 章和第 11 章,作者:John Whitington。

  • Introduction to Objective Caml, chapter 8, section 4
    Objective Caml 简介,第 8 章,第 4 节

  • Real World OCaml, chapters 13 and 18
    现实世界的 OCaml,第 13 章和第 18 章

  • Purely Functional Data Structures, by Chris Okasaki. Cambridge University Press, 1999.
    纯函数式的数据结构,作者:Chris Okasaki。剑桥大学出版社,1999。

8.9. Exercises 8.9. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: hash insert [★★]
练习:散列插入[★★]

Suppose we have a hash table on integer keys. The table currently has 7 empty buckets, and the hash function is simply let hash k = k mod 7. Draw the hash table that results from inserting the keys 4, 8, 15, 16, 23, and 42 (with whatever values you like).
假设我们有一个关于整数键的哈希表。该表当前有 7 个空桶,哈希函数很简单 let hash k = k mod 7 。绘制插入键 4、8、15、16、23 和 42(使用您喜欢的任何值)所产生的哈希表。


Exercise: relax bucket RI [★★]
练习:放松桶 RI [★★]

We required that hash table buckets must not contain duplicates. What would happen if we relaxed this RI to allow duplicates? Would the efficiency of any operations (insert, find, or remove) change?
我们要求哈希表桶不能包含重复项。如果我们放宽此 RI 以允许重复,会发生什么?任何操作(插入、查找或删除)的效率是否会改变?


Exercise: strengthen bucket RI [★★]
练习:强化桶 RI [★★]

What would happen if we strengthened the bucket RI to require each bucket to be sorted by the key? Would the efficiency of any operations (insert, find, or remove) change?
如果我们加强桶RI,要求每个桶按键排序,会发生什么?任何操作(插入、查找或删除)的效率是否会改变?


Exercise: hash values [★★]
练习:哈希值[★★]

Use Hashtbl.hash : 'a -> int to hash several values of different types. Make sure to try at least (), false, true, 0, 1, "", and [], as well as several “larger” values of each type. We saw that lists quickly can create collisions. Try creating binary trees and finding a collision.
使用 Hashtbl.hash : 'a -> int 对多个不同类型的值进行哈希处理。确保至少尝试 ()falsetrue01"" ,以及每种类型的几个“较大”值。我们发现列表很快就会产生冲突。尝试创建二叉树并查找冲突。


Exercise: hashtbl usage [★★]
练习:哈希表用法[★★]

Create a hash table tab with Hashtbl.create whose initial size is 16. Add 31 bindings to it with Hashtbl.add. For example, you could add the numbers 1..31 as keys and the strings “1”..”31” as their values. Use Hashtbl.find to look for keys that are in tab, as well as keys that are not.
使用 Hashtbl.create 创建一个哈希表 tab ,其初始大小为 16。使用 Hashtbl.add 为其添加 31 个绑定。例如,您可以添加数字 1..31 作为键,添加字符串“1”..”31”作为其值。使用 Hashtbl.find 查找 tab 中的键以及不在 tab 中的键。


Exercise: hashtbl stats [★]
练习:哈希表状态[★]

Use the Hashtbl.stats function to find out the statistics of tab (from an exercise above). How many buckets are in the table? How many buckets have a single binding in them?
使用 Hashtbl.stats 函数找出 tab 的统计信息(来自上面的练习)。表中有多少个桶?有多少个桶中有一个绑定?


Exercise: hashtbl bindings [★★]
练习:哈希表绑定[★★]

Define a function bindings : ('a,'b) Hashtbl.t -> ('a*'b) list, such that bindings h returns a list of all bindings in h. Use your function to see all the bindings in tab (from an exercise above). Hint: fold.
定义一个函数 bindings : ('a,'b) Hashtbl.t -> ('a*'b) list ,以便 bindings h 返回 h 中所有绑定的列表。使用您的函数查看 tab 中的所有绑定(来自上面的练习)。提示:折叠。


Exercise: hashtbl load factor [★★]
练习:哈希表负载因子[★★]

Define a function load_factor : ('a,'b) Hashtbl.t -> float, such that load_factor h is the load factor of h. What is the load factor of tab? Hint: stats.
定义一个函数 load_factor : ('a,'b) Hashtbl.t -> float ,使得 load_factor hh 的负载因子。 tab 的负载系数是多少?提示:统计。

Add one more binding to tab. Do the stats or load factor change? Now add yet another binding. Now do the stats or load factor change? Hint: Hashtbl resizes when the load factor goes strictly above 2.
tab 添加一个绑定。统计数据或负载因子是否发生变化?现在添加另一个绑定。现在统计数据或负载因子是否发生变化?提示:当负载因子严格高于 2 时,Hashtbl 会调整大小。


Exercise: functorial interface [★★★]
练习:函数式接口[★★★]

Use the functorial interface (i.e., Hashtbl.Make) to create a hash table whose keys are strings that are case insensitive. Be careful to obey the specification of Hashtbl.HashedType.hash:
使用函数接口(即 Hashtbl.Make )创建一个哈希表,其键是不区分大小写的字符串。请注意遵守 Hashtbl.HashedType.hash 的规范:

If two keys are equal according to equal, then they have identical hash values as computed by hash.
如果根据 equal 两个键相等,则它们具有由 hash 计算的相同哈希值。


Exercise: equals and hash [★★]
练习:相等与哈希[★★]

The previous exercise quoted the specification of Hashtbl.HashedType.hash. Compare that to Java’s Object.hashCode() specification. Why do they both have this similar requirement?
上一个练习引用了 Hashtbl.HashedType.hash 的规范。将其与 Java 的 Object.hashCode() 规范进行比较。为什么他们都有这样相似的要求?


Exercise: bad hash [★★] 练习:坏哈希[★★]

Use the functorial interface to create a hash table with a really bad hash function (e.g., a constant function). Use the stats function to see how bad the bucket distribution becomes.
使用 functorial 接口创建一个具有非常糟糕的哈希函数(例如常量函数)的哈希表。使用 stats 函数来查看存储桶分布的糟糕程度。


Exercise: linear probing [★★★★]
练习:线性探测[★★★★]

We briefly mentioned probing as an alternative to chaining. Probing can be effectively used in hardware implementations of hash tables, as well as in databases. With probing, every bucket contains exactly one binding. In case of a collision, we search forward through the array, as described below.
我们简要提到探测作为链接的替代方案。探测可以有效地用于哈希表的硬件实现以及数据库中。通过探测,每个桶只包含一个绑定。如果发生冲突,我们将在数组中向前搜索,如下所述。

Your task: Implement a hash table that uses linear probing. The details are below.
您的任务:实现一个使用线性探测的哈希表。详细信息如下。

Find. Suppose we are trying to find a binding in the table. We hash the binding’s key and look in the appropriate bucket. If there is already a different key in that bucket, we start searching forward through the array at the next bucket, then the next bucket, and so forth, wrapping back around to the beginning of the array if necessary. Eventually we will either
查找。假设我们试图在表中查找绑定。我们对绑定的密钥进行哈希处理并在适当的存储桶中查找。如果该存储桶中已经存在不同的键,则我们开始在下一个存储桶中向前搜索数组,然后是下一个存储桶,依此类推,如果有必要,则回绕到数组的开头。最终我们要么

  • find an empty bucket, in which case the key we’re searching for is not bound in the table;
    找到一个空桶,在这种情况下我们要搜索的键没有绑定在表中;

  • find the key before we reach an empty bucket, in which case we can return the value; or
    在到达空桶之前找到键,在这种情况下我们可以返回值;或者

  • never find the key or an empty bucket, instead wrapping back around to the original bucket, in which case all buckets are full and the key is not bound in the table. This case actually should never occur, because we won’t allow the load factor to get high enough for all buckets to be filled.
    永远不会找到密钥或空桶,而是回绕到原始桶,在这种情况下,所有桶都已满,并且密钥未绑定在表中。这种情况实际上不应该发生,因为我们不会让负载因子变得足够高以填充所有桶。

Insert. Insertion follows the same algorithm as finding a key, except that whenever we first find an empty bucket, we can insert the binding there.
插入。插入遵循与查找键相同的算法,只不过每当我们第一次找到一个空桶时,我们就可以在那里插入绑定。

Remove. Removal is more difficult. Once the key is found, we can’t just make the bucket empty, because that would affect future searches by causing them to stop early. Instead, we can introduce a special “deleted” value into that bucket to indicate that the bucket does not contain a binding but the searches should not stop at it.
删除。删除难度更大。一旦找到密钥,我们就不能直接将桶清空,因为这会导致以后的搜索提前停止,从而影响未来的搜索。相反,我们可以在该存储桶中引入一个特殊的“已删除”值,以指示该存储桶不包含绑定,但搜索不应停止于此。

Resizing. Since we never want the array to become completely full, we can keep the load factor near 1/4. When the load factor exceeds 1/2, we can double the array, bringing the load factor back to 1/4. When the load factor goes below 1/8, we can half the array, again bringing the load factor back to 1/4. “Deleted” bindings complicate the definition of load factor:
重整大小。由于我们不希望数组完全满,因此我们可以将负载因子保持在 1/4 附近。当负载因子超过 1/2 时,我们可以将阵列加倍,使负载因子回到 1/4 。当负载系数低于 1/8 时,我们可以将阵列减半,再次将负载系数恢复到 1/4。 “删除”的绑定使负载因子的定义变得复杂:

  • When determining whether to double the table size, we calculate the load factor as (# of bindings + # of deleted bindings) / (# of buckets). That is, deleted bindings contribute toward increasing the load factor.
    在确定是否将表大小加倍时,我们将负载因子计算为(绑定数 + 已删除绑定数)/(存储桶数)。也就是说,删除的绑定有助于增加负载因子。

  • When determining whether the half the table size, we calculate the load factor as (# of bindings) / (# buckets). That is, deleted bindings do not count toward increasing the load factor.
    当确定是否是表大小的一半时,我们将负载因子计算为(绑定数)/(存储桶数)。也就是说,删除的绑定不计入增加负载因子。

When rehashing the table, deleted bindings are of course not re-inserted into the new table.
重新散列表时,删除的绑定当然不会重新插入到新表中。


Exercise: functorized BST [★★★]
练习:函子化 BST [★★★]

Our implementation of BSTs assumed that it was okay to compare values using the built-in comparison operators <, =, and >. But what if the client wanted to use their own comparison operators? (e.g., to ignore case in strings, or to have sets of records where only a single field of the record was used for ordering.) Implement a BstSet abstraction as a functor parameterized on a structure that enables client-provided comparison operator(s), much like the standard library Set.
我们的 BST 实现假设可以使用内置比较运算符 <=> 来比较值。但是如果客户想使用他们自己的比较运算符怎么办? (例如,忽略字符串中的大小写,或者拥有仅使用记录的单个字段进行排序的记录集。)将 BstSet 抽象实现为在结构上参数化的函子,该结构使客户端能够提供比较运算符,非常类似于标准库 Set


Exercise: efficient traversal [★★★]
练习:高效遍历[★★★]

Suppose you wanted to convert a tree to a list. You’d have to put the values stored in the tree in some order. Here are three ways of doing that:
假设您想将树转换为列表。您必须将存储在树中的值按某种顺序放置。以下是三种方法:

  • preorder: each node’s value appears in the list before the values of its left then right subtrees.
    未序:每个节点的值出现在列表中其左子树和右子树的值之前。

  • inorder: the values of the left subtree appear, then the value at the node, then the values of the right subtree.
    中序:先出现左子树的值,然后是节点处的值,然后是右子树的值。

  • postorder: the values of a node’s left then right subtrees appear, followed by the value at the node.
    末序:出现节点的左子树和右子树的值,后面是该节点的值。

Here is code that implements those traversals, along with some example applications:
以下是实现这些遍历的代码以及一些示例应用程序:

type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree

let rec preorder = function
  | Leaf -> []
  | Node (l,v,r) -> [v] @ preorder l @ preorder r

let rec inorder = function
  | Leaf -> []
  | Node (l,v,r) ->  inorder l @ [v] @ inorder r

let rec postorder = function
  | Leaf -> []
  | Node (l,v,r) ->  postorder l @ postorder r @ [v]

let t =
  Node(Node(Node(Leaf, 1, Leaf), 2, Node(Leaf, 3, Leaf)),
       4,
       Node(Node(Leaf, 5, Leaf), 6, Node(Leaf, 7, Leaf)))

(*
  t is
        4
      /   \
     2     6
    / \   / \
   1   3 5   7
*)

let () = assert (preorder t  = [4;2;1;3;6;5;7])
let () = assert (inorder t   = [1;2;3;4;5;6;7])
let () = assert (postorder t = [1;3;2;5;7;6;4])

On unbalanced trees, the traversal functions above require quadratic worst-case time (in the number of nodes), because of the @ operator. Re-implement the functions without @, and instead using ::, such that they perform exactly one cons per Node in the tree. Thus the worst-case execution time will be linear. You will need to add an additional accumulator argument to each function, much like with tail recursion. (But your implementations won’t actually be tail recursive.)
在不平衡树上,由于 @ 运算符的存在,上述遍历函数需要二次最坏情况时间(以节点数计)。重新实现不使用 @ 的函数,而是使用 :: ,以便它们在树中的每个 Node 中只执行一个 cons。因此,最坏情况的执行时间将是线性的。您需要为每个函数添加一个额外的累加器参数,就像尾递归一样。 (但您的实现实际上不会是尾递归。)


Exercise: RB draw complete [★★]
练习:完整绘制红黑树[★★]

Draw the perfect binary tree on the values 1, 2, …, 15. Color the nodes in three different ways such that (i) each way is a red-black tree (i.e., satisfies the red-black invariants), and (ii) the three ways create trees with black heights of 2, 3, and 4, respectively. Recall that the black height of a tree is the maximum number of black nodes along any path from its root to a leaf.
在值 1、2、...、15 上绘制完美二叉树。以三种不同的方式为节点着色,使得 (i) 每种方式都是红黑树(即满足红黑不变量),并且 (ii )这三种方法分别创建黑色高度为 2、3 和 4 的树。回想一下,树的黑色高度是从根到叶子的任何路径上黑色节点的最大数量。


Exercise: RB draw insert [★★]
练习:绘制红黑树插入[★★]

Draw the red-black tree that results from inserting the characters D A T A S T R U C T U R E into an empty tree. Carry out the insertion algorithm yourself by hand, then check your work with the implementation provided in the book.
画出将字符 D A T A S T R U C T U R E 插入空树后得到的红黑树。自己手动执行插入算法,然后使用书中提供的实现检查您的工作。


Exercise: standard library set [★★]
练习:标准库里的集合[★★]

Read the source code of the standard library Set module. Find the representation invariant for the balanced trees that it uses. Which kind of tree does it most resemble: 2-3, AVL, or red-black?
阅读标准库 Set 模块的源代码。找到它使用的平衡树的表示不变性。它最像哪种树:2-3、AVL 或红黑树?


Exercise: pow2 [★★] 练习:pow2 [★★]

Using this type: 使用这种类型:

type 'a sequence = Cons of 'a * (unit -> 'a sequence)

Define a value pow2 : int sequence whose elements are the powers of two: <1; 2; 4; 8; 16, ...>.
定义一个值 pow2 : int sequence ,其元素是 2 的幂: <1; 2; 4; 8; 16, ...>


Exercise: more sequences [★★]
练习:更多序列[★★]

Define the following sequences:
定义以下序列:

  • the even naturals 均匀的自然物

  • the lower-case alphabet on endless repeat: a, b, c, …, z, a, b, …
    无限重复的小写字母:a、b、c、...、z、a、b、...

  • unending pseudorandom coin flips (e.g., booleans or a variant with Heads and Tails constructors)
    无休止的伪随机硬币翻转(例如,布尔值或带有 HeadsTails 构造函数的变体)


Exercise: nth [★★] 练习:第 n 个[★★]

Define a function nth : 'a sequence -> int -> 'a, such that nth s n the element at zero-based position n in sequence s. For example, nth pow2 0 = 1, and nth pow2 4 = 16.
定义一个函数 nth : 'a sequence -> int -> 'a ,取得 nth s n 序列 s 中从零开始的位置 n 处的元素。例如, nth pow2 0 = 1nth pow2 4 = 16


Exercise: hd tl [★★] 练习:头和尾[★★]

Explain how each of the following sequence expressions is evaluated:
解释如何计算以下每个序列表达式:

  • hd nats

  • tl nats

  • hd (tl nats)

  • tl (tl nats)

  • hd (tl (tl nats))


Exercise: filter [★★★] 练习:过滤器[★★★]

Define a function filter : ('a -> bool) -> 'a sequence -> 'a sequence, such that filter p s is the sub-sequence of s whose elements satisfy the predicate p. For example, filter (fun n -> n mod 2 = 0) nats would be the sequence <0; 2; 4; 6; 8; 10; ...>. If there is no element of s that satisfies p, then filter p s does not terminate.
定义一个函数 filter : ('a -> bool) -> 'a sequence -> 'a sequence ,使得 filter p ss 的子序列,其元素满足谓词 p 。例如, filter (fun n -> n mod 2 = 0) nats 将是序列 <0; 2; 4; 6; 8; 10; ...> 。如果 s 中没有满足 p 的元素,则 filter p s 不会终止。


Exercise: interleave [★★★]
练习:交错[★★★]

Define a function interleave : 'a sequence -> 'a sequence -> 'a sequence, such that interleave <a1; a2; a3; ...> <b1; b2; b3; ...> is the sequence <a1; b1; a2; b2; a3; b3; ...>. For example, interleave nats pow2 would be <0; 1; 1; 2; 2; 4; 3; 8; ...>
定义一个函数 interleave : 'a sequence -> 'a sequence -> 'a sequence ,使得 interleave <a1; a2; a3; ...> <b1; b2; b3; ...> 是序列 <a1; b1; a2; b2; a3; b3; ...> 。例如, interleave nats pow2 将是 <0; 1; 1; 2; 2; 4; 3; 8; ...>


Exercise: sift [★★★] 练习:筛选[★★★]

The Sieve of Eratosthenes is a way of computing the prime numbers.
埃拉托斯特尼筛法是一种计算素数的方法。

  • Start with the sequence <2; 3; 4; 5; 6; ...>.
    从序列 <2; 3; 4; 5; 6; ...> 开始。

  • Take 2 as prime. Delete all multiples of 2, since they cannot be prime. That leaves <3; 5; 7; 9; 11; ...>.
    取2为素数。删除所有 2 的倍数,因为它们不可能是素数。剩下 <3; 5; 7; 9; 11; ...>

  • Take 3 as prime and delete its multiples. That leaves <5; 7; 11; 13; 17; ...>.
    取3为质数并删除它的倍数。剩下 <5; 7; 11; 13; 17; ...>

  • Take 5 as prime, etc.
    取5为质数,以此类推。

Define a function sift : int -> int sequence -> int sequence, such that sift n s removes all multiples of n from s. Hint: filter.
定义一个函数 sift : int -> int sequence -> int sequence ,以便 sift n ss 中删除 n 的所有倍数。提示:过滤器


Exercise: primes [★★★] 练习:素数[★★★]

Define a sequence prime : int sequence, containing all the prime numbers starting with 2.
定义一个序列 prime : int sequence ,包含所有以 2 开头的素数。


Exercise: approximately e [★★★★]
练习:大约 e [★★★★]

The exponential function ex can be computed by the following infinite sum:
指数函数 ex 可以通过以下无限和计算:

ex=x00!+x11!+x22!+x33!++xkk!+

Define a function e_terms : float -> float sequence. Element k of the sequence should be term k from the infinite sum. For example, e_terms 1.0 is the sequence <1.0; 1.0; 0.5; 0.1666...; 0.041666...; ...>. The easy way to compute that involves a function that computes f(k)=xkk!.
定义一个函数 e_terms : float -> float sequence 。序列的元素 k 应该是无限和中的项 k 。例如, e_terms 1.0 是序列 <1.0; 1.0; 0.5; 0.1666...; 0.041666...; ...> 。简单的计算方法涉及计算 f(k)=xkk! 的函数。

Define a function total : float sequence -> float sequence, such that total <a; b; c; ...> is a running total of the input elements, i.e., <a; a+.b; a+.b+.c; ...>.
定义一个函数 total : float sequence -> float sequence ,使得 total <a; b; c; ...> 是输入元素的运行总计,即 <a; a+.b; a+.b+.c; ...>

Define a function within : float -> float sequence -> float, such that within eps s is the first element of s for which the absolute difference between that element and the element before it is strictly less than eps. If there is no such element, within is permitted not to terminate (i.e., go into an “infinite loop”). As a precondition, the tolerance eps must be strictly positive. For example, within 0.1 <1.0; 2.0; 2.5; 2.75; 2.875; 2.9375; 2.96875; ...> is 2.9375.
定义一个函数 within : float -> float sequence -> float ,使得 within eps ss 的第一个元素,该元素与其前面的元素之间的绝对差严格小于 eps 。如果没有这样的元素,则允许 within 不终止(即进入“无限循环”)。作为前提条件,容差 eps 必须严格为正。例如, within 0.1 <1.0; 2.0; 2.5; 2.75; 2.875; 2.9375; 2.96875; ...>2.9375

Finally, define a function e : float -> float -> float such that e x eps is ex computed to within a tolerance of eps, which must be strictly positive. Note that there is an interesting boundary case where x=1.0 for the first two terms of the sum; you could choose to drop the first term (which is always 1.0) from the sequence before using within.
最后,定义一个函数 e : float -> float -> float ,使得 e x eps 的计算 exeps 的容差范围内,该容差必须严格为正数。请注意,有一个有趣的边界情况,其中 x=1.0 代表总和的前两项;您可以选择在使用 within 之前从序列中删除第一项(始终为 1.0 )。


Exercise: better e [★★★★]
练习:更好的 e [★★★★]

Although the idea for computing ex above through the summation of an infinite series is good, the exact algorithm suggested above could be improved. For example, computing the 20th term in the sequence leads to a very large numerator and denominator if x is large. Investigate that behavior, comparing it to the built-in function exp : float -> float. Find a better way to structure the computation to improve the approximations you obtain. Hint: what if when computing term k you already had term k1? Then you could just do a single multiplication and division.
虽然上面通过无穷级数求和来计算 ex 的想法很好,但是上面建议的精确算法还可以改进。例如,如果 x 很大,则计算序列中的第 20 项会导致非常大的分子和分母。研究该行为,将其与内置函数 exp : float -> float 进行比较。找到一种更好的方法来构建计算,以改进获得的近似值。提示:如果在计算术语 k 时你已经有了术语 k1 该怎么办?然后你可以只进行一次乘法和除法。

Also, you could improve the test that within uses to determine whether two values are close. A good one for determining whether a and b are close might be relative distance:
此外,您还可以改进 within 用于确定两个值是否接近的测试。确定 ab 是否接近的一个好方法可能是相对距离:

|abmin(a,b)|<ϵ.

Exercise: different sequence rep [★★★]
练习:不同顺序重复[★★★]

Consider this alternative representation of sequences:
考虑序列的这种替代表示:

type 'a sequence = Cons of (unit -> 'a * 'a sequence)

How would you code up hd : 'a sequence -> 'a, tl : 'a sequence -> 'a sequence, nats : int sequence, and map : ('a -> 'b) -> 'a sequence -> 'b sequence for it? Explain how this representation is even lazier than our original representation.
您将如何为其编写 hd : 'a sequence -> 'atl : 'a sequence -> 'a sequencenats : int sequencemap : ('a -> 'b) -> 'a sequence -> 'b sequence 代码?解释一下这个表示比我们原来的表示更懒。


Exercise: lazy hello [★] 练习:惰性初见[★]

Define a value of type unit Lazy.t (which is synonymous with unit lazy_t), such that forcing that value with Lazy.force causes "Hello lazy world" to be printed. If you force it again, the string should not be printed.
定义 unit Lazy.t 类型的值(与 unit lazy_t 同义),这样用 Lazy.force 强制该值会导致打印 "Hello lazy world" 。如果再次强制执行,则不应打印该字符串。


Exercise: lazy and [★★] 运动:惰性与运算[★★]

Define a function (&&&) : bool Lazy.t -> bool Lazy.t -> bool. It should behave like a short circuit Boolean AND. That is, lb1 &&& lb2 should first force lb1. If it is false, the function should return false. Otherwise, it should force lb2 and return its value.
定义一个函数 (&&&) : bool Lazy.t -> bool Lazy.t -> bool 。它的行为应该类似于短路布尔 AND。也就是说, lb1 &&& lb2 应该首先强制 lb1 。如果是 false ,该函数应返回 false 。否则,它应该强制 lb2 并返回其值。


Exercise: lazy sequence [★★★]
练习:惰性序列[★★★]

Implement map and filter for the 'a lazysequence type provided in the section on laziness.
为惰性部分中提供的 'a lazysequence 类型实现 mapfilter


Exercise: promise and resolve [★★]
练习:承诺与解决[★★]

Use the finished version of the Promise module we developed to do the following: create a integer promise and resolver, bind a function on the promise to print the contents of the promise, then resolve the promise. Only after the promise is resolved should the printing occur.
使用我们开发的 Promise 模块的完成版本来执行以下操作:创建一个整数 Promise 和解析器,在 Promise 上绑定一个函数以打印 Promise 的内容,然后解析 Promise。只有在承诺得到解决后才可以进行打印。


Exercise: promise and resolve lwt [★★]
练习:承诺与解决在 lwt [★★]

Repeat the above exercise, but use the Lwt library instead of our own Promise library. Make sure to use Lwt’s I/O functions (e.g., Lwt_io.printf).
重复上述练习,但使用 Lwt 库而不是我们自己的 Promise 库。确保使用 Lwt 的 I/O 函数(例如 Lwt_io.printf )。


Exercise: timing challenge 1 [★★]
练习:计时挑战 1 [★★]

Here is a function that produces a time delay. We can use it to simulate an I/O call that takes a long time to complete.
这是一个产生时间延迟的函数。我们可以用它来模拟需要很长时间才能完成的 I/O 调用。

(** [delay s] is a promise that resolves after about [s] seconds. *)
let delay (sec : float) : unit Lwt.t =
  Lwt_unix.sleep sec

Write a function delay_then_print : unit -> unit Lwt.t that delays for three seconds then prints "done".
编写一个函数 delay_then_print : unit -> unit Lwt.t ,延迟三秒,然后打印 "done"


Exercise: timing challenge 2 [★★★]
练习:计时挑战 2 [★★★]

What happens when timing2 () is run? How long does it take to run? Make a prediction, then run the code to find out.
运行 timing2 () 时会发生什么?运行需要多长时间?做出预测,然后运行代码来找出答案。

open Lwt.Infix

let timing2 () =
  let _t1 = delay 1. >>= fun () -> Lwt_io.printl "1" in
  let _t2 = delay 10. >>= fun () -> Lwt_io.printl "2" in
  let _t3 = delay 20. >>= fun () -> Lwt_io.printl "3" in
  Lwt_io.printl "all done"

Exercise: timing challenge 3 [★★★]
练习:计时挑战 3 [★★★]

What happens when timing3 () is run? How long does it take to run? Make a prediction, then run the code to find out.
运行 timing3 () 时会发生什么?运行需要多长时间?做出预测,然后运行代码来找出答案。

open Lwt.Infix

let timing3 () =
  delay 1. >>= fun () ->
  Lwt_io.printl "1" >>= fun () ->
  delay 10. >>= fun () ->
  Lwt_io.printl "2" >>= fun () ->
  delay 20. >>= fun () ->
  Lwt_io.printl "3" >>= fun () ->
  Lwt_io.printl "all done"

Exercise: timing challenge 4 [★★★]
练习:计时挑战 4 [★★★]

What happens when timing4 () is run? How long does it take to run? Make a prediction, then run the code to find out.
运行 timing4 () 时会发生什么?运行需要多长时间?做出预测,然后运行代码来找出答案。

open Lwt.Infix

let timing4 () =
  let t1 = delay 1. >>= fun () -> Lwt_io.printl "1" in
  let t2 = delay 10. >>= fun () -> Lwt_io.printl "2" in
  let t3 = delay 20. >>= fun () -> Lwt_io.printl "3" in
  Lwt.join [t1; t2; t3] >>= fun () ->
  Lwt_io.printl "all done"

Exercise: file monitor [★★★★]
练习:文件监控[★★★★]

Write an Lwt program that monitors the contents of a file named “log”. Specifically, your program should open the file, continually read a line from the file, and as each line becomes available, print the line to stdout. When you reach the end of the file (EOF), your program should terminate cleanly without any exceptions.
编写一个 Lwt 程序来监视名为“log”的文件的内容。具体来说,您的程序应该打开文件,不断地从文件中读取一行,并且当每行可用时,将该行打印到标准输出。当到达文件末尾 (EOF) 时,程序应该干净地终止,没有任何异常。

Here is starter code:
这是起始代码:

open Lwt.Infix
open Lwt_io
open Lwt_unix

(** [log ()] is a promise for an [input_channel] that reads from
    the file named "log". *)
let log () : input_channel Lwt.t =
  openfile "log" [O_RDONLY] 0 >>= fun fd ->
  Lwt.return (of_fd input fd)

(** [loop ic] reads one line from [ic], prints it to stdout,
    then calls itself recursively. It is an infinite loop. *)
let rec loop (ic : input_channel) =
  failwith "TODO"
  (* hint: use [Lwt_io.read_line] and [Lwt_io.printlf] *)

(** [monitor ()] monitors the file named "log". *)
let monitor () : unit Lwt.t =
  log () >>= loop

(** [handler] is a helper function for [main]. If its input is
    [End_of_file], it handles cleanly exiting the program by
    returning the unit promise. Any other input is re-raised
    with [Lwt.fail]. *)
let handler : exn -> unit Lwt.t =
  failwith "TODO"

let main () : unit Lwt.t =
  Lwt.catch monitor handler

let _ = Lwt_main.run (main ())

Complete loop and handler. You might find the Lwt manual to be useful.
完成 loophandler 。您可能会发现 Lwt 手册很有用。

To compile your code, put it in a file named monitor.ml. Create a dune file for it:
要编译代码,请将其放入名为 monitor.ml 的文件中。为其创建一个沙丘文件:

(executable
 (name monitor)
 (libraries lwt.unix))

And run it as usual:
并像往常一样运行它:

$ dune exec ./monitor.exe

To simulate a file to which lines are being added over time, open a new terminal window and enter the following commands:
要模拟随时间添加行的文件,请打开一个新的终端窗口并输入以下命令:

$ mkfifo log
$ cat >log

Now anything you type into the terminal window (after pressing return) will be added to the file named log. That will enable you to interactively test your program.
现在,您在终端窗口中输入的任何内容(按回车键后)都将添加到名为 log 的文件中。这将使您能够交互式地测试您的程序。


Exercise: add opt [★★] 练习:加法运算符[★★]

Here are the definitions for the maybe monad:
以下是 Maybe monad 的定义:

module type Monad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
end

module Maybe : Monad =
struct
  type 'a t = 'a option

  let return x = Some x

  let ( >>= ) m f =
    match m with
    | Some x -> f x
    | None -> None

end

Implement add : int Maybe.t -> int Maybe.t -> int Maybe.t. If either of the inputs is None, then the output should be None. Otherwise, if the inputs are Some a and Some b then the output should be Some (a+b). The definition of add must be located outside of Maybe, as shown above, which means that your solution may not use the constructors None or Some in its code.
实施 add : int Maybe.t -> int Maybe.t -> int Maybe.t 。如果任一输入为 None ,则输出应为 None 。否则,如果输入是 Some aSome b ,那么输出应该是 Some (a+b)add 的定义必须位于 Maybe 之外,如上所示,这意味着您的解决方案不能使用构造函数 NoneSome


Exercise: fmap and join [★★]
练习:fmap 和 join [★★]

Here is an extended signature for monads that adds two new operations:
这是 monad 的扩展签名,添加了两个新操作:

module type ExtMonad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
  val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t
  val join : 'a t t -> 'a t
end

Just as the infix operator >>= is known as bind, the infix operator >>| is known as fmap. The two operators differ only in the return type of their function argument.
正如中缀运算符 >>= 称为 bind 一样,中缀运算符 >>| 称为 fmap 。这两个运算符仅在函数参数的返回类型上有所不同。

Using the box metaphor, >>| takes a boxed value, and a function that only knows how to work on unboxed values, extracts the value from the box, runs the function on it, and boxes up that output as its own return value.
使用盒子比喻, >>| 接受一个装箱值,以及一个只知道如何处理未装箱值的函数,从盒子中提取值,对其运行函数,然后将该输出装箱为它的值。自己的返回值。

Also using the box metaphor, join takes a value that is wrapped in two boxes and removes one of the boxes.
同样使用盒子比喻, join 获取一个包含在两个盒子中的值并删除其中一个盒子。

It’s possible to implement >>| and join directly with pattern matching (as we already implemented >>=). It’s also possible to implement them without pattern matching.
可以直接通过模式匹配来实现 >>|join (因为我们已经实现了 >>= )。也可以在没有模式匹配的情况下实现它们。

For this exercise, do the former: implement >>| and join as part of the Maybe monad, and do not use >>= or return in the body of >>| or join.
对于本练习,执行前者:将 >>|join 实现为 Maybe monad 的一部分,并且不要使用 >>=return>>|join 的主体中。


Exercise: fmap and join again [★★]
练习:再次 fmap 和 join [★★]

Solve the previous exercise again. This time, you must use >>= and return to implement >>| and join, and you may not use Some or None in the body of >>| and join.
再次解决之前的练习。这次,必须使用 >>=return 来实现 >>|join ,并且不能使用 SomeNone>>|join 的主体中。


Exercise: bind from fmap+join [★★★]
练习:来自 fmap 加 join 的绑定 [★★★]

The previous exercise demonstrates that >>| and join can be implemented entirely in terms of >>= (and return), without needing to know anything about the representation type 'a t of the monad.
前面的练习演示了 >>|join 可以完全根据 >>= (和 return )来实现,而无需了解任何内容关于 monad 的表示类型 'a t

It’s actually possible to go the other direction. That is, >>= can be implemented using just >>| and join, without needing to know anything about the representation type 'a t.
其实也可以往另一个方向走。也就是说, >>= 可以仅使用 >>|join 来实现,而不需要了解有关表示类型 'a t 的任何信息。

Prove that this is so by completing the following code:
通过完成以下代码来证明这一点:

module type FmapJoinMonad = sig
  type 'a t
  val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t
  val join : 'a t t -> 'a t
  val return : 'a -> 'a t
end

module type BindMonad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
end

module MakeMonad (M : FmapJoinMonad) : BindMonad = struct
  (* TODO *)
end

Hint: let the types be your guide.
提示:让类型作为您的指南。


Exercise: list monad [★★★]
练习:列表单子[★★★]

We’ve seen three examples of monads already; let’s examine a fourth, the list monad. The “something more” that it does is to upgrade functions to work on lists instead of just single values. (Note, there is no notion of concurrency intended here. It’s not that the list monad runs functions concurrently on every element of a list. The Lwt monad does, however, provide that kind of functionality.)
我们已经看到了 monad 的三个例子;让我们检查第四个,列表单子。它所做的“更多事情”是升级函数以在列表上工作,而不仅仅是单个值。 (注意,这里没有并发的概念。列表 monad 并不是在列表的每个元素上同时运行函数。但是,Lwt monad 确实提供了这种功能。)

For example, suppose you have these functions:
例如,假设您有以下功能:

let inc x = x + 1
let pm x = [x; -x]

Then the list monad could be used to apply those functions to every element of a list and return the result as a list. For example,
然后列表 monad 可用于将这些函数应用于列表的每个元素并将结果作为列表返回。例如,

  • [1; 2; 3] >>| inc is [2; 3; 4].
    [1; 2; 3] >>| inc[2; 3; 4]

  • [1; 2; 3] >>= pm is [1; -1; 2; -2; 3; -3].
    [1; 2; 3] >>= pm[1; -1; 2; -2; 3; -3]

  • [1; 2; 3] >>= pm >>| inc is [2; 0; 3; -1; 4; -2].
    [1; 2; 3] >>= pm >>| inc[2; 0; 3; -1; 4; -2]

One way to think about this is that the list monad operators take a list of inputs to a function, run the function on all those inputs, and give you back the combined list of outputs.
思考这个问题的一种方法是,列表 monad 运算符将一系列输入传递给函数,对所有这些输入运行该函数,然后返回输出的组合列表。

Complete the following definition of the list monad:
完成列表 monad 的以下定义:

module type ExtMonad = sig
  type 'a t
  val return : 'a -> 'a t
  val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
  val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t
  val join : 'a t t -> 'a t
end

module ListMonad : ExtMonad = struct
  type 'a t = 'a list

  (* TODO *)
end

Hints: Leave >>= for last. Let the types be your guide. There are two very useful list library functions that can help you.
提示:将 >>= 留到最后。让类型成为您的指南。有两个非常有用的列表库函数可以帮助您。


Exercise: trivial monad laws [★★★]
练习:琐碎的单子定律 [★★★]

Here is the world’s most trivial monad. All it does is wrap a value inside of a constructor.
这是世界上最琐碎的单子。它所做的只是将一个值包装在构造函数内。

module Trivial : Monad = struct
  type 'a t = Wrap of 'a
  let return x = Wrap x
  let ( >>= ) (Wrap x) f = f x
end

Prove that the three monad laws, as formulated using >>= and return, hold for the trivial monad.
证明使用 >>=return 制定的三个单子定律对于平凡单子成立。

9. Interpreters 9. 解释器 ¶

A skilled artisan must understand the tools with which they work. A carpenter needs to understand saws and planes. A chef needs to understand knives and pots. A programmer, among other tools, needs to understand the compilers that implement the programming languages they use.
熟练的工匠必须了解他们工作时使用的工具。木匠需要了解锯子和刨子。厨师需要了解刀具和锅具。除了其他工具之外,程序员还需要了解实现他们所使用的编程语言的编译器。

A full understanding of compilation requires a full course or two. So here, we’re going to take a necessarily brief look at how to implement programming languages. The goal is to understand some of the basic implementation techniques, so as to demystify the tools you’re using. Although you might never need to implement a full general-purpose programming language, it’s highly likely that at some point in your career you will want to design and implement some small, special-purpose language. Sometimes those are called domain-specific languages (DSLs). What we cover here should help you with that task.
充分理解编译需要一两门完整的课程。因此,在这里,我们将简要介绍一下如何实现编程语言。目标是了解一些基本的实现技术,以便揭开您正在使用的工具的神秘面纱。尽管您可能永远不需要实现完整的通用编程语言,但在您职业生涯的某个阶段,您很可能想要设计和实现一些小型的专用语言。有时这些被称为特定领域语言(DSL)。我们在这里介绍的内容应该可以帮助您完成这项任务。

A compiler is a program that implements a programming language. So is an interpreter. But they differ in their implementation strategy.
编译器是实现编程语言的程序。口译员也是如此。但它们的实施策略有所不同。

A compiler’s primary task is translation. It takes as input a source program and produces as output a target program. The source program is typically expressed in a high-level language, such as Java or OCaml. The target program is typically expressed in a low-level language, such as MIPS or x86 assembly. Then the compiler’s job is done, and it is no longer needed. Later the OS helps to load and execute the target program. Typically, a compiler results in higher-performance implementations.
编译器的首要任务是翻译。它将源程序作为输入并生成目标程序作为输出。源程序通常用高级语言表达,例如Java或OCaml。目标程序通常用低级语言表达,例如MIPS或x86汇编。那么编译器的工作就完成了,不再需要它了。随后操作系统帮助加载并执行目标程序。通常,编译器会产生更高性能的实现。

An interpreter’s primary task is execution. It takes as input a source program and directly executes that program without producing any target program. The OS actually loads and executes the interpreter, and the interpreter is then responsible for executing the program. Typically, an interpreter is easier to implement than a compiler.
解释器的首要任务是执行。它以源程序作为输入并直接执行该程序,而不生成任何目标程序。操作系统实际上加载并执行解释器,然后解释器负责执行程序。通常,解释器比编译器更容易实现。

It’s also possible to implement a language using a mixture of compilation and interpretation. The most common example of that involves virtual machines that execute bytecode, such as the Java Virtual Machine (JVM) or the OCaml virtual machine (which used to be called the Zinc Machine). With this strategy, a compiler translates the source language into bytecode, and the virtual machine interprets the bytecode.
还可以使用编译和解释的混合来实现语言。最常见的示例涉及执行字节码的虚拟机,例如 Java 虚拟机 (JVM) 或 OCaml 虚拟机(以前称为 Zinc Machine)。通过这种策略,编译器将源语言翻译为字节码,然后虚拟机解释字节码。

High-performance virtual machines, such as Java’s HotSpot, take this a step further and embed a compiler inside the virtual machine. When the machine notices that a piece of bytecode is being interpreted frequently, it uses the compiler to translate that bytecode into the language of the machine (e.g., x86) on which the machine is running. This is called just-in-time compilation (JIT), because code is being compiled just before it is executed.
高性能虚拟机(例如 Java 的 HotSpot)更进一步,在虚拟机中嵌入了编译器。当机器注意到一段字节码被频繁解释时,它会使用编译器将该字节码翻译成该机器正在运行的机器的语言(例如,x86)。这称为即时编译 (JIT),因为代码在执行之前进行编译。

A compiler goes through several phases as it translates a program:
编译器在翻译程序时会经历几个阶段:

Lexing. During lexing, the compiler transforms the original source code of the program from a sequence of characters to a sequence of tokens. Tokens are adjacent characters that have some meaning when grouped together. You might think of them analogously to words in a natural language. Indeed, keywords such as if and match would be tokens in OCaml. So would constants such as 42 and "hello", variable names such as x and lst, and punctuation such as (, ), and ->. Lexing typically removes whitespace, because it is no longer needed once the tokens have been identified. (Though in a whitespace-sensitive language like Python, it would need to be preserved.)
词法分析。在词法分析期间,编译器将程序的原始源代码从字符序列转换为标记序列。标记是组合在一起时具有一定含义的相邻字符。您可能会认为它们类似于自然语言中的单词。事实上,诸如 ifmatch 之类的关键字将是 OCaml 中的标记。 42"hello" 等常量、 xlst 等变量名以及 (-> 。词法分析通常会删除空格,因为一旦识别出标记就不再需要空格。 (尽管在像 Python 这样的空白敏感语言中,它需要被保留。)

Parsing. During parsing, the compiler transforms the sequence of tokens into a tree called the abstract syntax tree (AST). As the name suggests, this tree abstracts from the concrete syntax of the language. Recall that abstraction can mean “forgetting about details.” The AST typically forgets about concrete details. For example:
解析。在解析过程中,编译器将标记序列转换为称为抽象语法树(AST)的树。顾名思义,这棵树从语言的具体语法中抽象出来。回想一下,抽象可能意味着“忘记细节”。 AST 通常会忘记具体的细节。例如:

  • In 1 + (2 + 3) the parentheses group the right-hand addition operation, indicating it should be evaluated first. A tree can represent that as follows:
    1 + (2 + 3) 中的括号将右侧加法运算分组,表示应首先对其求值。一棵树可以表示如下:

       +
      / \
     1   +
        / \
       2   3
    

    Parentheses are no longer needed, because the structure of the tree encodes them.
    不再需要括号,因为树的结构对它们进行了编码。

  • In [1; 2; 3], the square brackets delineate the beginning and end of the list, and the semicolons separate the list elements. A tree could represent that as a node with several children:
    [1; 2; 3] 中,方括号界定列表的开头和结尾,分号分隔列表元素。一棵树可以将其表示为具有多个子节点的节点:

       list
      /  |  \
     1   2   3
    

    The brackets and semicolons are no longer needed.
    不再需要括号和分号。

  • In fun x -> 42, the fun keyword and -> punctuation mark separate the arguments and body of the function from the surrounding code. A tree can represent that as a node with two children:
    fun x -> 42 中, fun 关键字和 -> 标点符号将函数的参数和主体与周围的代码分开。一棵树可以将其表示为具有两个子节点的节点:

      function
      /     \
     x       42
    

    The keyword and punctuation are no longer needed.
    不再需要关键字和标点符号。

An AST thus represents the structure of a program at a level that is easier for the compiler writer to manipulate.
因此,AST 在编译器编写者更容易操作的级别上表示程序的结构。

Semantic analysis. During semantic analysis, the compiler checks to see whether the program is meaningful according to the rules of the language that the compiler is implementing. The most common kind of semantic analysis is type checking: the compiler analyzes the types of all the expressions that appear in the program to see whether there is a type error or not. Type checking typically requires producing a data structure called a symbol table that maps identifiers (e.g., variable names) to their types. As a new scope is entered, the symbol table is extended with new bindings that might shadow old bindings; and as the scope is exited, the new bindings are removed, thus restoring the old bindings. So a symbol table blends features of a dictionary and a stack data structure.
语义分析。在语义分析期间,编译器根据编译器正在实现的语言的规则检查程序是否有意义。最常见的语义分析是类型检查:编译器分析程序中出现的所有表达式的类型,以查看是否存在类型错误。类型检查通常需要生成一个称为符号表的数据结构,该数据结构将标识符(例如变量名称)映射到其类型。当进入新的作用域时,符号表会使用新的绑定进行扩展,这些新的绑定可能会影响旧的绑定;当退出作用域时,新的绑定将被删除,从而恢复旧的绑定。因此,符号表混合了字典和堆栈数据结构的特征。

Besides type checking, there are other kinds of semantic analysis. Examples include the following:
除了类型检查之外,还有其他类型的语义分析。示例包括以下内容:

  • checking whether the branches of an OCaml pattern match are exhaustive,
    检查 OCaml 模式匹配的分支是否详尽,

  • checking whether a C break keyword occurs within the body of a loop, and
    检查 C break 关键字是否出现在循环体内,以及

  • checking whether a Java field marked final has been initialized by the end of a constructor.
    检查标记为 final 的 Java 字段是否已在构造函数末尾初始化。

You can think of parsing as “checking to see whether a program is meaningful”—which is how we just defined semantic analysis. So the distinction between parsing and semantic analysis is more about convenience: parsing does enough work to implement the production of an AST, and semantic analysis does the rest of the work.
您可以将解析视为“检查程序是否有意义”——这就是我们刚刚定义语义分析的方式。因此,解析和语义分析之间的区别更多的是便利性:解析完成了足够的工作来实现 AST 的生成,而语义分析完成了其余的工作。

Sometimes semantic analysis is even necessary to fully determine what the AST should be! Consider, for example, the expression (foo) - bar in a C-like language. It might be:
有时甚至需要语义分析才能完全确定 AST 应该是什么!例如,考虑类 C 语言中的表达式 (foo) - bar 。有可能:

  • the unary negation of a variable bar, cast to the type foo, or
    变量 bar 的一元否定,转换为类型 foo ,或者

  • the binary subtraction operation with operands foo and bar, where the parentheses were gratuitous.
    使用操作数 foobar 进行二进制减法运算,其中括号是无意义的。

Until enough semantic analysis has been done to figure out whether foo is a variable name or a type name, the compiler doesn’t know which AST to generate. In such situations, the parser typically produces an AST in which some tree nodes represent the ambiguous syntax, then the semantic analysis phase rewrites the tree to be unambiguous.
在进行足够的语义分析以确定 foo 是变量名还是类型名之前,编译器不知道要生成哪个 AST。在这种情况下,解析器通常会生成一个 AST,其中一些树节点表示不明确的语法,然后语义分析阶段将树重写为明确的。

Translation to intermediate representation. After semantic analysis, a compiler could immediately translate the AST (augmented with symbol tables) into the target language. But if the same compiler wanted to produce output for multiple targets (e.g., for x86 and ARM and MIPS), that would require defining a translation from the AST to each of the targets. In practice, compilers typically don’t do that. Instead, they first translate the AST to an intermediate representation (IR). Think of the IR as a kind of abstraction of many assembly languages. Many source languages (e.g., C, Java, OCaml) could be translated to the same IR, and from that IR, many target language outputs (e.g., x86, ARM, MIPS) could be produced.
翻译为中间表示。经过语义分析后,编译器可以立即将 AST(用符号表增强)翻译成目标语言。但如果同一个编译器想要为多个目标(例如,x86、ARM 和 MIPS)生成输出,则需要定义从 AST 到每个目标的转换。实际上,编译器通常不会这样做。相反,他们首先将 AST 转换为中间表示 (IR)。将 IR 视为许多汇编语言的一种抽象。许多源语言(例如,C、Java、OCaml)可以翻译为相同的 IR,并且可以从该 IR 生成许多目标语言输出(例如,x86、ARM、MIPS)。

An IR language typically has abstract machine instructions that accomplish conceptually simple tasks: loading from or storing to memory, performing binary operations, calling and returning, and jumping to other instructions. The abstract machine typically has an unbounded number of registers available for use, much like a source program can have an unbounded number of variables. Real machines, however, have a finite number of registers, which is one way in which the IR is an abstraction.
IR 语言通常具有抽象机器指令,用于完成概念上简单的任务:从内存加载或存储到内存、执行二进制操作、调用和返回以及跳转到其他指令。抽象机通常具有无限数量的可供使用的寄存器,就像源程序可以具有无限数量的变量一样。然而,真实机器的寄存器数量是有限的,这是 IR 抽象的一种方式。

Target code generation. The final phase of compilation is to generate target code from the IR. This phase typically involves selecting concrete machine instructions (such as x86 opcodes), and determining which variables will be stored in memory (which is slow to access) vs. processor registers (which are fast to access but limited in number). As part of code generation, a compiler therefore attempts to optimize the performance of the target code. Some examples of optimizations include:
目标码生成。编译的最后阶段是从 IR 生成目标代码。此阶段通常涉及选择具体的机器指令(例如 x86 操作码),并确定哪些变量将存储在内存(访问速度慢)和处理器寄存器(访问速度快但数量有限)中。因此,作为代码生成的一部分,编译器尝试优化目标代码的性能。一些优化示例包括:

  • eliminating array bounds checks, if they are provably guaranteed to succeed;
    如果可以证明数组边界检查能够成功,则消除它们;

  • eliminating redundant computations;
    消除冗余计算;

  • replacing a function call with the body of the function itself, suitably instantiated on the arguments, to eliminate the overhead of calling and returning; and
    用函数本身的主体替换函数调用,在参数上适当实例化,以消除调用和返回的开销;和

  • re-ordering machine instructions so that (e.g.) slow reads from memory are begun before their results are needed, and doing other instructions in the meanwhile that do not need the result of the read.
    重新排序机器指令,以便(例如)在需要其结果之前开始从内存中缓慢读取,并同时执行不需要读取结果的其他指令。

Groups of Phases. The phases of compilation can be grouped into two or three pieces:
阶段组。编译的阶段可以分为两个或三个部分:

  • The front end of the compiler does lexing, parsing, and semantic analysis. It produces an AST and associated symbol tables. It transforms the AST into an IR.
    编译器的前端进行词法分析、解析和语义分析。它生成 AST 和关联的符号表。它将 AST 转换为 IR。

  • The middle end (if it exists) of the compiler operates on the IR. Usually this involves performing optimizations that are independent of the target language.
    编译器的中端(如果存在)对 IR 进行操作。通常这涉及执行独立于目标语言的优化。

  • The back end of the compiler does code generation, including further optimization.
    编译器的后端进行代码生成,包括进一步优化。

Interpretation Phases. An interpreter works like the front (and possibly middle) end of a compiler. That is, an interpreter does lexing, parsing, and semantic analysis. It might then immediately begin executing the AST, or it might transform the AST into an IR and begin executing the IR.
解释阶段。解释器的工作方式类似于编译器的前端(也可能是中间)。也就是说,解释器进行词法分析、语法分析和语义分析。然后,它可能会立即开始执行 AST,或者可能会将 AST 转换为 IR 并开始执行 IR。

In the rest of this book, we are going to focus on interpreters. We’ll ignore IRs and code generation, and instead study how to directly execute the AST.
在本书的其余部分,我们将重点关注口译员。我们将忽略 IR 和代码生成,而是研究如何直接执行 AST。

Note 笔记

Because of the additional tooling required, the code in this chapter is not runnable in a browser like previous chapters. But we do provide downloadable code for each interpreter implemented here.
由于需要额外的工具,本章中的代码无法像前面的章节一样在浏览器中运行。但我们确实为此处实现的每个解释器提供了可下载的代码。

9.1. Example: Calculator
9.1. 示例:计算器 ¶

Let’s start with a video guided tour of implementing an interpreter for a tiny language: just a calculator, essentially, with addition and multiplication. The point of this guided tour is not to go into great detail about any single piece of it. Rather, the goal is to get a little familiarity with the OCaml tools and techniques for lexing, parsing, and evaluation. They are all rather tightly coupled, which makes it challenging to understand one piece without having a high-level understanding of the whole. After we get that understanding from the tour, we’ll start over again in the next section (on parsing), and at that time we’ll dive into the details.
让我们从视频引导开始,了解如何为一种小型语言实现解释器:本质上只是一个计算器,具有加法和乘法功能。这次导游的目的不是要详细介绍其中的任何一个部分。相反,我们的目标是稍微熟悉一下用于词法分析、解析和评估的 OCaml 工具和技术。它们都相当紧密地耦合在一起,这使得在没有对整体有高层次理解的情况下理解其中一个部分变得具有挑战性。在我们从导览中获得理解后,我们将在下一节(解析)中重新开始,届时我们将深入研究细节。

9.2. Parsing 9.2. 解析 ¶

You could code your own lexer and parser from scratch. But many languages include tools for automatically generating lexers and parsers from formal descriptions of the syntax of a language. The ancestors of many of those tools are lex and yacc, which generate lexers and parsers, respectively; lex and yacc were developed in the 1970s for C.
您可以从头开始编写自己的词法分析器和解析器。但许多语言都包含根据语言语法的正式描述自动生成词法分析器和解析器的工具。其中许多工具的祖先是 lex 和 yacc,它们分别生成词法分析器和解析器; lex 和 yacc 是在 20 世纪 70 年代为 C 语言开发的。

As part of the standard distribution, OCaml provides lexer and parser generators named ocamllex and ocamlyacc. There is a more modern parser generator named menhir available through opam; menhir is “90% compatible” with ocamlyacc and provides significantly improved support for debugging generated parsers.
作为标准发行版的一部分,OCaml 提供了名为 ocamllex 和 ocamlyacc 的词法分析器和解析器生成器。有一个更现代的解析器生成器,名为 menhir,可以通过 opam 获得; menhir 与 ocamlyacc “90% 兼容”,并为调试生成的解析器提供了显着改进的支持。

9.2.1. Lexers 9.2.1. 词法分析器 ¶

Lexer generators such as lex and ocamllex are built on the theory of deterministic finite automata, which is typically covered in a discrete math or theory of computation course. Such automata accept regular languages, which can be described with regular expressions. So, the input to a lexer generator is a collection of regular expressions that describe the tokens of the language. The output is an automaton implemented in a high-level language, such as C (for lex) or OCaml (for ocamllex).
lex 和 ocamllex 等词法分析器生成器建立在确定性有限自动机理论的基础上,该理论通常包含在离散数学或计算理论课程中。这种自动机接受正则语言,可以用正则表达式来描述。因此,词法分析器生成器的输入是描述语言标记的正则表达式的集合。输出是用高级语言实现的自动机,例如 C(对于 lex)或 OCaml(对于 ocamllex)。

That automaton itself takes files (or strings) as input, and each character of the file becomes an input to the automaton. Eventually the automaton either recognizes the sequence of characters it has received as a valid token in the language, in which case the automaton produces an output of that token and resets itself to being recognizing the next token, or rejects the sequence of characters as an invalid token.
该自动机本身将文件(或字符串)作为输入,并且文件的每个字符都成为自动机的输入。最终,自动机要么将其接收到的字符序列识别为语言中的有效标记,在这种情况下,自动机生成该标记的输出并将其自身重置为识别下一个标记,要么将字符序列视为无效而拒绝令牌。

9.2.2. Parsers 9.2.2. 解析器 ¶

Parser generators such as yacc and menhir are similarly built on the theory of automata. But they use pushdown automata, which are like finite automata that also maintain a stack onto which they can push and pop symbols. The stack enables them to accept a bigger class of languages, which are known as context-free languages (CFLs). One of the big improvements of CFLs over regular languages is that CFLs can express the idea that delimiters must be balanced—for example, that every opening parenthesis must be balanced by a closing parenthesis.
yacc 和 menhir 等解析器生成器同样是基于自动机理论构建的。但他们使用下推自动机,就像有限自动机一样,也维护一个堆栈,可以在其中压入和弹出符号。该堆栈使它们能够接受更大类别的语言,这些语言称为上下文无关语言 (CFL)。 CFL 相对于常规语言的一大改进是 CFL 可以表达分隔符必须平衡的想法,例如,每个左括号必须由右括号平衡。

Just as regular languages can be expressed with a special notation (regular expressions), so can CFLs. Context-free grammars are used to describe CFLs. A context-free grammar is a set of production rules that describe how one symbol can be replaced by other symbols. For example, the language of balanced parentheses, which includes strings such as (()) and ()() and (()()), but not strings such as ) or ((), is generated by these rules:
正如常规语言可以用特殊符号(正则表达式)来表达一样,CFL 也可以。上下文无关语法用于描述 CFL。上下文无关语法是一组产生式规则,描述一个符号如何被其他符号替换。例如,平衡括号的语言,包括 (())()()(()()) 等字符串,但不包括 ) 等字符串或 (() ,由以下规则生成:

  • S(S)

  • SSS

  • Sϵ

The symbols occurring in those rules are S, (, and ). The ϵ denotes the empty string. Every symbol is either a nonterminal or a terminal, depending on whether it is a token of the language being described. S is a nonterminal in the example above, and ( and ) are terminals.
这些规则中出现的符号是 S()ϵ 表示空字符串。每个符号要么是非终结符,要么是终结符,具体取决于它是否是所描述语言的标记。上例中 S 是非终结符, ( 和 ) 是终结符。

In the next section we’ll study Backus-Naur Form (BNF), which is a standard notation for context-free grammars. The input to a parser generator is typically a BNF description of the language’s syntax. The output of the parser generator is a program that recognizes the language of the grammar. As input, that program expects the output of the lexer. As output, the program produces a value of the AST type that represents the string that was accepted. The programs output by the parser generator and lexer generator are thus dependent upon on another and upon the AST type.
在下一节中,我们将学习巴科斯-诺尔范式(BNF),它是上下文无关语法的标准表示法。解析器生成器的输入通常是语言语法的 BNF 描述。解析器生成器的输出是识别语法语言的程序。该程序需要词法分析器的输出作为输入。作为输出,程序会生成一个 AST 类型的值,表示已接受的字符串。因此,解析器生成器和词法分析器生成器输出的程序依赖于另一个生成器和 AST 类型。

9.2.3. Backus-Naur Form
9.2.3. 巴科斯-诺尔范式

The standard way to describe the syntax of a language is with a mathematical notation called Backus-Naur form (BNF), named for its inventors, John Backus and Peter Naur. There are many variants of BNF. Here, we won’t be too picky about adhering to one variant or another. Our goal is just to have a reasonably good notation for describing language syntax.
描述语言语法的标准方法是使用称为巴科斯-诺尔形式 (BNF) 的数学符号,以其发明者约翰·巴克斯和彼得·诺尔命名。 BNF 有多种变体。在这里,我们不会太挑剔坚持一种或另一种变体。我们的目标只是拥有一个相当好的符号来描述语言语法。

BNF uses a set of derivation rules to describe the syntax of a language. Let’s start with an example. Here’s the BNF description of a tiny language of expressions that include just the integers and addition:
BNF 使用一组推导规则来描述语言的语法。让我们从一个例子开始。以下是一种小型表达式语言的 BNF 描述,其中仅包含整数和加法:

e ::= i | e + e
i ::= <integers>

These rules say that an expression e is either an integer i, or two expressions with the symbol + appearing between them. The syntax of “integers” is left unspecified by these rules.
这些规则规定表达式 e 要么是整数 i ,要么是两个表达式之间出现符号 + 。这些规则未指定“整数”的语法。

Each rule has the form
每个规则都有以下形式

metavariable ::= symbols | ... | symbols

A metavariable is variable used in the BNF rules, rather than a variable in the language being described. The ::= and | that appear in the rules are metasyntax: BNF syntax used to describe the language’s syntax. Symbols are sequences that can include metavariables (such as i and e) as well as tokens of the language (such as +). Whitespace is not relevant in these rules.
元变量是 BNF 规则中使用的变量,而不是所描述的语言中的变量。规则中出现的 ::=| 是元语法:用于描述语言语法的BNF语法。符号是可以包含元变量(例如 ie )以及语言标记(例如 + )的序列。空格与这些规则无关。

Sometimes we might want to easily refer to individual occurrences of metavariables. We do that by appending some distinguishing mark to the metavariable(s). For example, we could rewrite the first rule above as
有时我们可能想轻松引用元变量的个别出现。我们通过向元变量附加一些区别标记来做到这一点。例如,我们可以将上面的第一条规则重写为

e ::= i | e1 + e2

or as 或作为

e ::= i | e + e'

Now we can talk about e2 or e' rather than having to say “the e on the right-hand side of +”.
现在我们可以谈论 e2e' 而不必说“ + 右侧的 e ”。

If the language itself contains either of the tokens ::= or |—and OCaml does contain the latter—then writing BNF can become a little confusing. Some BNF notations attempt to deal with that by using additional delimiters to distinguish syntax from metasyntax. We will be more relaxed and assume that the reader can distinguish them.
如果语言本身包含标记 ::=| (并且 OCaml 确实包含后者),那么编写 BNF 可能会变得有点混乱。一些 BNF 表示法试图通过使用额外的分隔符来区分语法和元语法来解决这个问题。我们会更加放松,假设读者能够区分它们。

9.2.4. Example: SimPL 9.2.4. 示例:简单的 PL ¶

As a running example, we’ll use a very simple programming language that we call SimPL. Here is its syntax in BNF:
作为一个运行示例,我们将使用一种非常简单的编程语言,称为 SimPL。以下是 BNF 格式的语法:

e ::= x | i | b | e1 bop e2
    | if e1 then e2 else e3
    | let x = e1 in e2

bop ::= + | * | <=

x ::= <identifiers>

i ::= <integers>

b ::= true | false

Obviously there’s a lot missing from this language, especially functions. But there’s enough in it for us to study the important concepts of interpreters without getting too distracted by lots of language features. Later, we will consider a larger fragment of OCaml.
显然,这种语言缺少很多东西,尤其是函数。但其中的内容足以让我们研究口译员的重要概念,而不会被大量的语言特征分散注意力。稍后,我们将考虑 OCaml 的更大片段。

We’re going to develop a complete interpreter for SimPL. You can download the finished interpreter here: simpl.zip. Or, just follow along as we build each piece of it.
我们将为 SimPL 开发一个完整的解释器。您可以在此处下载完成的解释器:simpl.zip。或者,只要跟着我们构建它的每个部分即可。

9.2.4.1. The AST 9.2.4.1. AST ¶

Since the AST is the most important data structure in an interpreter, let’s design it first. We’ll put this code in a file named ast.ml:
由于 AST 是解释器中最重要的数据结构,所以我们先来设计它。我们将此代码放入名为 ast.ml 的文件中:

type bop =
  | Add
  | Mult
  | Leq

type expr =
  | Var of string
  | Int of int
  | Bool of bool
  | Binop of bop * expr * expr
  | Let of string * expr * expr
  | If of expr * expr * expr

There is one constructor for each of the syntactic forms of expressions in the BNF. For the underlying primitive syntactic classes of identifiers, integers, and booleans, we’re using OCaml’s own string, int, and bool types.
BNF 中表达式的每种语法形式都有一个构造函数。对于标识符、整数和布尔值的底层基本语法类,我们使用 OCaml 自己的 stringintbool 类型。

Instead of defining the bop type and a single Binop constructor, we could have defined three separate constructors for the three binary operators:
我们可以为三个二元运算符定义三个单独的构造函数,而不是定义 bop 类型和单个 Binop 构造函数:

type expr =
  ...
  | Add of expr * expr
  | Mult of expr * expr
  | Leq of expr * expr
  ...

But by factoring out the bop type we will be able to avoid a lot of code duplication later in our implementation.
但是通过分解 bop 类型,我们将能够在稍后的实现中避免大量代码重复。

9.2.4.2. The Menhir Parser
9.2.4.2. Menhir 解析器 ¶

Let’s start with parsing, then return to lexing later. We’ll put all the Menhir code we write below in a file named parser.mly. The .mly extension indicates that this file is intended as input to Menhir. (The ‘y’ alludes to yacc.) This file contains the grammar definition for the language we want to parse. The syntax of grammar definitions is described by example below. Be warned that it’s maybe a little weird, but that’s because it’s based on tools (like yacc) that were developed quite awhile ago. Menhir will process that file and produce a file named parser.ml as output; it contains an OCaml program that parses the language. (There’s nothing special about the name parser here; it’s just descriptive.)
让我们从解析开始,然后再回到词法分析。我们将把下面编写的所有 Menhir 代码放在名为 parser.mly 的文件中。 .mly 扩展名表明该文件旨在作为 Menhir 的输入。 (‘y’暗指 yacc。)该文件包含我们要解析的语言的语法定义。下面通过示例描述语法定义的语法。请注意,这可能有点奇怪,但那是因为它基于很久以前开发的工具(如 yacc)。 Menhir 将处理该文件并生成一个名为 parser.ml 的文件作为输出;它包含一个解析该语言的 OCaml 程序。 (这里的名称 parser 没有什么特别的;它只是描述性的。)

There are four parts to a grammar definition: header, declarations, rules, and trailer.
语法定义有四个部分:标头、声明、规则和尾部。

Header. The header appears between %{ and %}. It is code that will be copied literally into the generated parser.ml. Here we use it just to open the Ast module so that, later on in the grammar definition, we can write expressions like Int i instead of Ast.Int i. If we wanted we could also define some OCaml functions in the header.
标头。标头出现在 %{%} 之间。该代码将被逐字复制到生成的 parser.ml 中。这里我们使用它只是打开 Ast 模块,以便稍后在语法定义中,我们可以编写像 Int i 这样的表达式而不是 Ast.Int i 。如果我们愿意,我们还可以在标头中定义一些 OCaml 函数。

%{
open Ast
%}

Declarations. The declarations section begins by saying what the lexical tokens of the language are. Here are the token declarations for SimPL:
声明。声明部分首先说明该语言的词汇标记是什么。以下是 SimPL 的令牌声明:

%token <int> INT
%token <string> ID
%token TRUE
%token FALSE
%token LEQ
%token TIMES
%token PLUS
%token LPAREN
%token RPAREN
%token LET
%token EQUALS
%token IN
%token IF
%token THEN
%token ELSE
%token EOF

Each of these is just a descriptive name for the token. Nothing so far says that LPAREN really corresponds to (, for example. We’ll take care of that when we define the lexer.
其中每一个都只是令牌的描述性名称。例如,到目前为止,没有任何内容表明 LPAREN 确实对应于 ( 。当我们定义词法分析器时,我们会处理这个问题。

The EOF token is a special end-of-file token that the lexer will return when it comes to the end of the character stream. At that point we know the complete program has been read.
EOF 标记是一种特殊的文件结束标记,词法分析器在到达字符流末尾时将返回该标记。此时我们知道完整的程序已被读取。

The tokens that have a <type> annotation appearing in them are declaring that they will carry some additional data along with them. In the case of INT, that’s an OCaml int. In the case of ID, that’s an OCaml string.
带有 <type> 注释的标记声明它们将携带一些附加数据。对于 INT 来说,这是一个 OCaml int 。对于 ID 来说,这是一个 OCaml string

After declaring the tokens, we have to provide some additional information about precedence and associativity. The following declarations say that PLUS is left associative, IN is not associative, and PLUS has higher precedence than IN (because PLUS appears on a line after IN).
声明标记后,我们必须提供一些有关优先级和关联性的附加信息。以下声明表示 PLUS 是左关联的, IN 不是关联的,并且 PLUS 的优先级高于 IN (因为 PLUS 出现在 IN 之后的一行上)。

%nonassoc IN
%nonassoc ELSE
%left LEQ
%left PLUS
%left TIMES

Because PLUS is left associative, 1 + 2 + 3 will parse as (1 + 2) + 3 and not as 1 + (2 + 3). Because PLUS has higher precedence than IN, the expression let x = 1 in x + 2 will parse as let x = 1 in (x + 2) and not as (let x = 1 in x) + 2. The other declarations have similar effects.
由于 PLUS 是左关联的,因此 1 + 2 + 3 将解析为 (1 + 2) + 3 而不是 1 + (2 + 3) 。由于 PLUS 的优先级高于 IN ,因此表达式 let x = 1 in x + 2 将解析为 let x = 1 in (x + 2) 而不是 (let x = 1 in x) + 2 。其他声明也有类似的效果。

Getting the precedence and associativity declarations correct is one of the trickier parts of developing a grammar definition. It helps to develop the grammar definition incrementally, adding just a couple tokens (and their associated rules, discussed below) at a time to the language. Menhir will let you know when you’ve added a token (and rule) for which it is confused about what you intend the precedence and associativity should be. Then you can add declarations and test to make sure you’ve got them right.
正确获取优先级和关联性声明是开发语法定义中比较棘手的部分之一。它有助于逐步开发语法定义,一次仅向语言添加几个标记(及其相关规则,如下所述)。当您添加了一个标记(和规则)时,Menhir 会通知您,而该标记(和规则)对于您想要的优先级和关联性应该是什么感到困惑。然后您可以添加声明并进行测试以确保它们正确。

After declaring associativity and precedence, we need to declare what the starting point is for parsing the language. The following declaration says to start with a rule (defined below) named prog. The declaration also says that parsing a prog will return an OCaml value of type Ast.expr.
声明关联性和优先级后,我们需要声明解析语言的起点。以下声明表示从名为 prog 的规则(定义如下)开始。该声明还表示解析 prog 将返回 Ast.expr 类型的 OCaml 值。

%start <Ast.expr> prog

Finally, %% ends the declarations section.
最后, %% 结束声明部分。

%%

Rules. The rules section contains production rules that resemble BNF, although where in BNF we would write “::=” these rules simply write “:”. The format of a rule is
规则。规则部分包含类似于 BNF 的产生式规则,尽管在 BNF 中我们会写“::=”,但这些规则只是写“:”。规则的格式为

name:
  | production1 { action1 }
  | production2 { action2 }
  | ...
  ;

The production is the sequence of symbols that the rule matches. A symbol is either a token or the name of another rule. The action is the OCaml value to return if a match occurs. Each production can bind the value carried by a symbol and use that value in its action. This is perhaps best understood by example, so let’s dive in.
产生式是规则匹配的符号序列。符号可以是标记,也可以是另一个规则的名称。该操作是匹配发生时返回的 OCaml 值。每个产生式都可以绑定符号所携带的值,并在其操作中使用该值。通过示例也许可以最好地理解这一点,所以让我们深入了解一下。

The first rule, named prog, has just a single production. It says that a prog is an expr followed by EOF. The first part of the production, e=expr, says to match an expr and bind the resulting value to e. The action simply says to return that value e.
第一条规则名为 prog ,只有一个产生式。它表示 progexpr 后跟 EOF 。产生式的第一部分 e=expr 表示匹配 expr 并将结果值绑定到 e 。该操作只是表示返回该值 e

prog:
  | e = expr; EOF { e }
  ;

The second and final rule, named expr, has productions for all the expressions in SimPL.
第二条也是最后一条规则,名为 expr ,具有 SimPL 中所有表达式的产生式。

expr:
  | i = INT { Int i }
  | x = ID { Var x }
  | TRUE { Bool true }
  | FALSE { Bool false }
  | e1 = expr; LEQ; e2 = expr { Binop (Leq, e1, e2) }
  | e1 = expr; TIMES; e2 = expr { Binop (Mult, e1, e2) }
  | e1 = expr; PLUS; e2 = expr { Binop (Add, e1, e2) }
  | LET; x = ID; EQUALS; e1 = expr; IN; e2 = expr { Let (x, e1, e2) }
  | IF; e1 = expr; THEN; e2 = expr; ELSE; e3 = expr { If (e1, e2, e3) }
  | LPAREN; e=expr; RPAREN {e}
  ;
  • The first production, i = INT, says to match an INT token, bind the resulting OCaml int value to i, and return AST node Int i.
    第一个产生式 i = INT 表示匹配 INT 标记,将生成的 OCaml int 值绑定到 i ,并返回 AST 节点 Int i

  • The second production, x = ID, says to match an ID token, bind the resulting OCaml string value to x, and return AST node Var x.
    第二个产生式 x = ID 表示匹配 ID 标记,将生成的 OCaml string 值绑定到 x ,并返回 AST 节点 Var x

  • The third and fourth productions match a TRUE or FALSE token and return the corresponding AST node.
    第三个和第四个产生式匹配 TRUEFALSE 标记并返回相应的 AST 节点。

  • The fifth, sixth, and seventh productions handle binary operators. For example, e1 = expr; PLUS; e2 = expr says to match an expr followed by a PLUS token followed by another expr. The first expr is bound to e1 and the second to e2. The AST node returned is Binop (Add, e1, e2).
    第五、第六和第七产生式处理二元运算符。例如, e1 = expr; PLUS; e2 = expr 表示匹配 expr 后跟 PLUS 标记,后跟另一个 expr 。第一个 expr 绑定到 e1 ,第二个绑定到 e2 。返回的 AST 节点是 Binop (Add, e1, e2)

  • The eighth production, LET; x = ID; EQUALS; e1 = expr; IN; e2 = expr, says to match a LET token followed by an ID token followed by an EQUALS token followed by an expr followed by an IN token followed by another expr. The string carried by the ID is bound to x, and the two expressions are bound to e1 and e2. The AST node returned is Let (x, e1, e2).
    第八个产生式 LET; x = ID; EQUALS; e1 = expr; IN; e2 = expr 表示匹配 LET 标记,后跟 ID 标记,后跟 EQUALS 标记,后跟跟随了一个 IN 标记的 expr ,后跟另一个 exprID 携带的字符串绑定到 x ,两个表达式绑定到 e1e2 。返回的 AST 节点是 Let (x, e1, e2)

  • The last production, LPAREN; e = expr; RPAREN says to match an LPAREN token followed by an expr followed by an RPAREN. The expression is bound to e and returned.
    最后一个产生式 LPAREN; e = expr; RPAREN 表示匹配 LPAREN 标记,后跟 expr ,后跟 RPAREN 。该表达式绑定到 e 并返回。

The final production might be surprising, because it was not included in the BNF we wrote for SimPL. That BNF was intended to describe the abstract syntax of the language, so it did not include the concrete details of how expressions can be grouped with parentheses. But the grammar definition we’ve been writing does have to describe the concrete syntax, including details like parentheses.
最终的结果可能会令人惊讶,因为它没有包含在我们为 SimPL 编写的 BNF 中。 BNF 旨在描述该语言的抽象语法,因此它不包括如何用括号对表达式进行分组的具体细节。但我们一直在编写的语法定义确实必须描述具体的语法,包括括号等细节。

There can also be a trailer section after the rules, which like the header is OCaml code that is copied directly into the output parser.ml file.
规则后面还可以有一个预告片部分,它像标题一样是直接复制到输出 parser.ml 文件中的 OCaml 代码。

9.2.4.3. The Ocamllex Lexer
9.2.4.3. Ocamllex 词法分析器 ¶

Now let’s see how the lexer generator is used. A lot of it will feel familiar from our discussion of the parser generator. We’ll put all the ocamllex code we write below in a file named lexer.mll. The .mll extension indicates that this file is intended as input to ocamllex. (The ‘l’ alludes to lexing.) This file contains the lexer definition for the language we want to lex. Menhir will process that file and produce a file named lexer.ml as output; it contains an OCaml program that lexes the language. (There’s nothing special about the name lexer here; it’s just descriptive.)
现在让我们看看词法分析器生成器是如何使用的。从我们对解析器生成器的讨论中,很多内容都会让人感到熟悉。我们将把下面编写的所有 ocamllex 代码放在名为 lexer.mll 的文件中。 .mll 扩展名表明该文件旨在作为 ocamllex 的输入。 (“l”暗示词法分析。)该文件包含我们想要词法分析的语言的词法分析器定义。 Menhir 将处理该文件并生成一个名为 lexer.ml 的文件作为输出;它包含一个对该语言进行词法分析的 OCaml 程序。 (这里的名称 lexer 没有什么特别的;它只是描述性的。)

There are four parts to a lexer definition: header, identifiers, rules, and trailer.
词法分析器定义由四个部分组成:标头、标识符、规则和尾部。

Header. The header appears between { and }. It is code that will simply be copied literally into the generated lexer.ml.
标头。标头出现在 {} 之间。该代码将简单地按字面复制到生成的 lexer.ml 中。

{
open Parser
}

Here, we’ve opened the Parser module, which is the code in parser.ml that was produced by Menhir out of parser.mly. The reason we open it is so that we can use the token names declared in it, e.g., TRUE, LET, and INT, inside our lexer definition. Otherwise, we’d have to write Parser.TRUE, etc.
在这里,我们打开了 Parser 模块,它是 parser.ml 中的代码,由 Menhir 从 parser.mly 中生成。我们打开它的原因是我们可以在词法分析器定义中使用其中声明的标记名称,例如 TRUELETINT 。否则,我们必须编写 Parser.TRUE 等。

Identifiers. The next section of the lexer definition contains identifiers, which are named regular expressions. These will be used in the rules section, next.
标识。词法分析器定义的下一部分包含标识符,称为正则表达式。这些将在接下来的规则部分中使用。

Here are the identifiers we’ll use with SimPL:
以下是我们将在 SimPL 中使用的标识符:

let white = [' ' '\t']+
let digit = ['0'-'9']
let int = '-'? digit+
let letter = ['a'-'z' 'A'-'Z']
let id = letter+

The regular expressions above are for whitespace (spaces and tabs), digits (0 through 9), integers (nonempty sequences of digits, optionally preceded by a minus sign), letters (a through z, and A through Z), and SimPL variable names (nonempty sequences of letters) aka ids or “identifiers”—though we’re now using that word in two different senses.
上面的正则表达式适用于空格(空格和制表符)、数字(0 到 9)、整数(非空数字序列,前面可以选择减号)、字母(a 到 z 和 A 到 Z)和 SimPL 变量名称(非空字母序列)又名 id 或“标识符”——尽管我们现在在两种不同的意义上使用该词。

FYI, these aren’t exactly the same as the OCaml definitions of integers and identifiers.
仅供参考,这些与整数和标识符的 OCaml 定义并不完全相同。

The identifiers section actually isn’t required; instead of writing white in the rules we could just directly write the regular expression for it. But the identifiers help make the lexer definition more self-documenting.
标识符部分实际上不是必需的;我们可以直接为其编写正则表达式,而不是在规则中编写 white 。但标识符有助于使词法分析器定义更加自记录。

Rules. The rules section of a lexer definition is written in a notation that also resembles BNF. A rule has the form
规则。词法分析器定义的规则部分是用也类似于 BNF 的表示法编写的。规则的形式为

rule name =
  parse
  | regexp1 { action1 }
  | regexp2 { action2 }
  | ...

Here, rule and parse are keywords. The lexer that is generated will attempt to match against regular expressions in the order they are listed. When a regular expression matches, the lexer produces the token specified by its action.
这里, ruleparse 是关键字。生成的词法分析器将尝试按照正则表达式列出的顺序进行匹配。当正则表达式匹配时,词法分析器会生成由其 action 指定的标记。

Here is the (only) rule for the SimPL lexer:
这是 SimPL 词法分析器的(唯一)规则:

rule read =
  parse
  | white { read lexbuf }
  | "true" { TRUE }
  | "false" { FALSE }
  | "<=" { LEQ }
  | "*" { TIMES }
  | "+" { PLUS }
  | "(" { LPAREN }
  | ")" { RPAREN }
  | "let" { LET }
  | "=" { EQUALS }
  | "in" { IN }
  | "if" { IF }
  | "then" { THEN }
  | "else" { ELSE }
  | id { ID (Lexing.lexeme lexbuf) }
  | int { INT (int_of_string (Lexing.lexeme lexbuf)) }
  | eof { EOF }

Most of the regular expressions and actions are self-explanatory, but a couple are not:
大多数正则表达式和操作都是不言自明的,但有一些则不然:

  • The first, white { read lexbuf }, means that if whitespace is matched, instead of returning a token the lexer should just call the read rule again and return whatever token results. In other words, whitespace will be skipped.
    第一个 white { read lexbuf } 意味着如果空格匹配,则词法分析器不应返回标记,而应再次调用 read 规则并返回任何标记结果。换句话说,空白将被跳过。

  • The two for ids and ints use the expression Lexing.lexeme lexbuf. This calls a function lexeme defined in the Lexing module, and returns the string that matched the regular expression. For example, in the id rule, it would return the sequence of upper and lower case letters that form the variable name.
    id 和 int 的两个使用表达式 Lexing.lexeme lexbuf 。这将调用 Lexing 模块中定义的函数 lexeme ,并返回与正则表达式匹配的字符串。例如,在 id 规则中,它将返回构成变量名称的大小写字母序列。

  • The eof regular expression is a special one that matches the end of the file (or string) being lexed.
    eof 正则表达式是一种特殊的正则表达式,它匹配正在词法分析的文件(或字符串)的末尾。

Note that it’s important that the id regular expression occur nearly last in the list. Otherwise, keywords like true and if would be lexed as variable names rather than the TRUE and IF tokens.
请注意, id 正则表达式几乎出现在列表的最后,这一点很重要。否则,像 trueif 这样的关键字将被词法为变量名称,而不是 TRUEIF 标记。

9.2.4.4. Generating the Parser and Lexer
9.2.4.4. 生成解析器和词法分析器 ¶

Now that we have completed parser and lexer definitions in parser.mly and lexer.mll, we can run Menhir and ocamllex to generate the parser and lexer from them. Let’s organize our code like this:
现在我们已经完成了 parser.mlylexer.mll 中的解析器和词法分析器定义,我们可以运行 Menhir 和 ocamllex 来生成解析器和词法分析器。让我们像这样组织我们的代码:

- <some root folder>
  - dune-project
  - src
    - ast.ml
    - dune
    - lexer.mll
    - parser.mly

In src/dune, write the following:
src/dune 中,写入以下内容:

(library
 (name interp))

(menhir
 (modules parser))

(ocamllex lexer)

That organizes the entire src folder into a library named Interp. The parser and lexer will be modules Interp.Parser and Interp.Lexer in that library.
这会将整个 src 文件夹组织到名为 Interp 的库中。解析器和词法分析器将是该库中的模块 Interp.ParserInterp.Lexer

Run dune build to compile the code, thus generating the parser and lexer. If you want to see the generated code, look in _build/default/src/ for parser.ml and lexer.ml.
运行 dune build 编译代码,从而生成解析器和词法分析器。如果您想查看生成的代码,请在 _build/default/src/ 中查找 parser.mllexer.ml

9.2.4.5. The Driver 9.2.4.5. 驱动 ¶

Finally, we can pull together the lexer and parser to transform a string into an AST. Put this code into a file named src/main.ml:
最后,我们可以将词法分析器和解析器结合起来,将字符串转换为 AST。将此代码放入名为 src/main.ml 的文件中:

open Ast

let parse (s : string) : expr =
  let lexbuf = Lexing.from_string s in
  let ast = Parser.prog Lexer.read lexbuf in
  ast

This function takes a string s and uses the standard library’s Lexing module to create a lexer buffer from it. Think of that buffer as the token stream. The function then lexes and parses the string into an AST, using Lexer.read and Parser.prog. The function Lexer.read corresponds to the rule named read in our lexer definition, and the function Parser.prog to the rule named prog in our parser definition.
此函数接受字符串 s 并使用标准库的 Lexing 模块从中创建词法分析器缓冲区。将该缓冲区视为令牌流。然后,该函数使用 Lexer.readParser.prog 将字符串词法分析并解析为 AST。函数 Lexer.read 对应于词法​​分析器定义中名为 read 的规则,函数 Parser.prog 对应于解析器中名为 prog 的规则定义。

Note how this code runs the lexer on a string; there is a corresponding function from_channel to read from a file.
请注意此代码如何在字符串上运行词法分析器;有一个相应的函数 from_channel 来从文件中读取。

We could now use parse interactively to parse some strings. Start utop and load the library declared in src with this command:
我们现在可以交互地使用 parse 来解析一些字符串。启动 utop 并使用以下命令加载 src 中声明的库:

$ dune utop src

Now Interp.Main.parse is available for use:
现在 Interp.Main.parse 可供使用:

# Interp.Main.parse "let x = 3110 in x + x";;
- : Interp.Ast.expr =
Interp.Ast.Let ("x", Interp.Ast.Int 3110,
 Interp.Ast.Binop (Interp.Ast.Add, Interp.Ast.Var "x", Interp.Ast.Var "x"))

That completes lexing and parsing for SimPL.
这就完成了 SimPL 的词法分析和解析。

9.3. Substitution Model
9.3. 替代模型 ¶

After lexing and parsing, the next phase is type checking (and other semantic analysis). We will skip that phase for now and return to it at the end of this chapter.
在词法分析和解析之后,下一阶段是类型检查(和其他语义分析)。我们暂时跳过该阶段,并在本章末尾返回该阶段。

Instead, let’s turn our attention to evaluation. In a compiler, the next phase after semantic analysis would be rewriting the AST into an intermediate representation (IR), in preparation for translating the program into machine code. An interpreter might also rewrite the AST into an IR, or it might directly begin evaluating the AST. One reason to rewrite the AST would be to simplify it: sometimes, certain language features can be implemented in terms of others, and it makes sense to reduce the language to a small core to keep the interpreter implementation shorter. Syntactic sugar is a great example of that idea.
相反,让我们把注意力转向评估。在编译器中,语义分析后的下一阶段是将 AST 重写为中间表示 (IR),为将程序翻译为机器代码做好准备。解释器也可能将 AST 重写为 IR,或者可能直接开始评估 AST。重写 AST 的一个原因是简化它:有时,某些语言功能可以根据其他语言功能来实现,并且将语言缩减为一个小核心以保持解释器实现更短是有意义的。语法糖就是这个想法的一个很好的例子。

Eliminating syntactic sugar is called desugaring. As an example, we know that let x = e1 in e2 and (fun x -> e2) e1 are equivalent. So, we could regard let expressions as syntactic sugar.
消除语法糖称为脱糖。例如,我们知道 let x = e1 in e2(fun x -> e2) e1 是等效的。因此,我们可以将 let 表达式视为语法糖。

Suppose we had a language whose AST corresponded to this BNF:
假设我们有一种语言,其 AST 对应于这个 BNF:

e ::= x | fun x -> e | e1 e2
    | let x = e1 in e2

Then the interpreter could desugar that into a simpler AST—in a sense, an IR—by transforming all occurrences of let x = e1 in e2 into (fun x -> e2) e1. Then the interpreter would need to evaluate only this smaller language:
然后,解释器可以将所有出现的 let x = e1 in e2 转换为 (fun x -> e2) e1 ,将其脱糖为更简单的 AST(某种意义上是 IR)。那么解释器只需要评估这种较小的语言:

e ::= x | fun x -> e | e1 e2

After having simplified the AST, it’s time to evaluate it. Evaluation is the process of continuing to simplify the AST until it’s just a value. In other words, evaluation is the implementation of the language’s dynamic semantics. Recall that a value is an expression for which there is no computation remaining to be done. Typically, we think of values as a strict syntactic subset of expressions, though we’ll see some exceptions to that later.
简化 AST 后,是时候对其进行评估了。评估是继续简化 AST 直到它只是一个值的过程。换句话说,求值是语言动态语义的实现。回想一下,值是一个没有剩余计算需要完成的表达式。通常,我们将值视为表达式的严格语法子集,尽管稍后我们会看到一些例外。

Big vs. small step evaluation. We’ll define evaluation with a mathematical relation, just as we did with type checking. Actually, we’re going to define three relations for evaluation:
大步评估 vs 小步评估。我们将使用数学关系来定义评估,就像我们对类型检查所做的那样。实际上,我们将定义三个关系进行评估:

  • The first, -->, will represent how a program takes one single step of execution.
    第一个 --> 表示程序如何执行单个步骤。

  • The second, -->*, is the reflexive transitive closure of -->, and it represents how a program takes multiple steps of execution.
    第二个 -->*--> 的自反传递闭包,它表示程序如何执行多个步骤。

  • The third, ==>, abstracts away from all the details of single steps and represents how a program reduces directly to a value.
    第三个 ==> 抽象了单个步骤的所有细节,并表示程序如何直接简化为一个值。

The style in which we are defining evaluation with these relations is known as operational semantics, because we’re using the relations to specify how the machine “operates” as it evaluates programs. There are two other major styles, known as denotational semantics and axiomatic semantics, but we won’t cover those here.
我们用这些关系定义评估的风格被称为操作语义,因为我们使用这些关系来指定机器在评估程序时如何“操作”。还有另外两种主要风格,称为指称语义和公理语义,但我们不会在这里介绍它们。

We can further divide operational semantics into two separate sub-styles of defining evaluation: small step vs. big step semantics. The first relation, -->, is in the small-step style, because it represents execution in terms of individual small steps. The third, ==>, is in the big-step style, because it represents execution in terms of a big step from an expression directly to a value. The second relation, -->*, blends the two. Indeed, our desire is for it to bridge the gap in the following sense:
我们可以进一步将操作语义分为两种独立的定义评估子样式:小步语义与大步语义。第一个关系 --> 是小步骤样式,因为它代表单个小步骤的执行。第三个 ==> 是大步风格,因为它表示从表达式直接到值的大步执行。第二个关系 -->* 混合了两者。事实上,我们希望它能够在以下意义上弥合差距:

Relating big and small steps: For all expressions e and values v, it holds that e -->* v if and only if e ==> v.
关联大小步:对于所有表达式 e 和值 v ,当且仅当 e ==> v 时,它才保持 e -->* v

In other words, if an expression takes many small steps and eventually reaches a value, e.g., e --> e1 --> .... --> en --> v, then it ought to be the case that e ==> v. So the big step relation is a faithful abstraction of the small step relation: it just forgets about all the intermediate steps.
换句话说,如果一个表达式采取许多小步骤并最终达到一个值,例如 e --> e1 --> .... --> en --> v ,那么它应该是 e ==> v 。因此,大步关系是小步关系的忠实抽象:它只是忘记了所有中间步骤。

Why have two different styles, big and small? Each is a little easier to use than the other in certain circumstances, so it helps to have both in our toolkit. The small-step semantics tends to be easier to work with when it comes to modeling complicated language features, but the big-step semantics tends to be more similar to how an interpreter would actually be implemented.
为什么有大、小两种不同的款式?在某些情况下,每种方法都比另一种更容易使用,因此将两者都包含在我们的工具包中会有所帮助。在对复杂的语言特征进行建模时,小步语义往往更容易使用,但大步语义往往更类似于解释器的实际实现方式。

Substitution vs. environment models. There’s another choice we have to make, and it’s orthogonal to the choice of small vs. big step. There are two different ways to think about the implementation of variables:
替代模型 vs 环境模型。我们还必须做出另一个选择,它与小步骤和大步骤的选择正交。有两种不同的方式来考虑变量的实现:

  • We could eagerly substitute the value of a variable for its name throughout the scope of that name, as soon as we find a binding of the variable.
    一旦我们找到变量的绑定,我们就可以在整个名称范围内急切地用变量的值替换它的名称。

  • We could lazily record the substitution in a dictionary, which is usually called an environment when used for this purpose, and we could look up the variable’s value in that environment whenever we find its name mentioned in a scope.
    我们可以懒惰地将替换记录在字典中,当用于此目的时,字典通常称为环境,并且每当我们在范围中找到变量的名称时,我们就可以在该环境中查找变量的值。

Those ideas lead to the substitution model of evaluation and the environment model of evaluation. As with small step vs. big step, the substitution model tends to be nicer to work with mathematically, whereas the environment model tends to be more similar to how an interpreter is implemented.
这些思想导致了评价的替代模型和评价的环境模型。与小步与大步一样,替换模型在数学上往往更适合使用,而环境模型往往更类似于解释器的实现方式。

Some examples will help to make sense of all this. Let’s look, next, at how to define the relations for SimPL.
一些例子将有助于理解这一切。接下来让我们看看如何定义 SimPL 的关系。

9.3.1. Evaluating SimPL in the Substitution Model
9.3.1. 在替代模型中评估 SimPL ¶

Let’s begin by defining a small-step substitution-model semantics for SimPL. That is, we’re going to define a relation --> that represents how an expression take a single step at a time, and we’ll implement variables using substitution of values for names.
让我们首先为 SimPL 定义一个小步替换模型语义。也就是说,我们将定义一个关系 --> 来表示表达式如何一次执行一步,并且我们将使用值替换名称来实现变量。

Recall the syntax of SimPL:
回想一下 SimPL 的语法:

e ::= x | i | b | e1 bop e2
    | if e1 then e2 else e3
    | let x = e1 in e2

bop ::= + | * | <=

We’re going to need to know when expressions are done evaluating, that is, when they are considered to be values. For SimPL, we’ll define the values as follows:
我们需要知道表达式何时完成评估,即何时将它们视为值。对于 SimPL,我们将定义值如下:

v ::= i | b

That is, a value is either an integer constant or a Boolean constant.
也就是说,值可以是整数常量,也可以是布尔常量。

For each of the syntactic forms that a SimPL expression could have, we’ll now define some evaluation rules, which constitute an inductive definition of the --> relation. Each rule will have the form e --> e', meaning that e takes a single step to e'.
对于 SimPL 表达式可能具有的每种语法形式,我们现在将定义一些评估规则,这些规则构成 --> 关系的归纳定义。每个规则的形式都是 e --> e' ,这意味着 e 需要一步到 e'

Although variables are given first in the BNF, let’s pass over them for now, and come back to them after all the other forms.
虽然 BNF 中首先给出了变量,但我们现在先忽略它们,然后在所有其他形式之后再回到它们。

Constants. Integer and Boolean constants are already values, so they cannot take a step. That might at first seem surprising, but remember that we are intending to also define a -->* relation that will permit zero or more steps; whereas, the --> relation represents exactly one step.
常数。整数和布尔常量已经是值,因此它们无法迈出一步。乍一看这可能令人惊讶,但请记住,我们还打算定义一个允许零个或多个步骤的 -->* 关系;而 --> 关系仅代表一个步骤。

Technically, all we have to do to accomplish this is to just not write any rules of the form i --> e or b --> e for some e. So we’re already done, actually: we haven’t defined any rules yet.
从技术上讲,要实现此目的,我们所要做的就是不为某些 e 编写任何 i --> eb --> e 形式的规则。所以实际上我们已经完成了:我们还没有定义任何规则。

Let’s introduce another notation written e -/->, which is meant to look like an arrow with a slash through it, to mean “there does not exist an e' such that e --> e'. Using that we could write:
让我们引入另一种表示法 e -/-> ,它看起来像一个带有斜线的箭头,表示“不存在 e' 使得 e --> e' .使用它我们可以写:

  • i -/->

  • b -/->

Though not strictly speaking part of the definition of -->, those propositions help us remember that constants do not step. In fact, we could more generally write, “for all v, it holds that v -/->.”
虽然严格来说不是 --> 定义的一部分,但这些命题帮助我们记住常量不会步进。事实上,我们可以更普遍地写,“对于所有 v ,它都支持 v -/-> 。”

Binary operators. A binary operator application e1 bop e2 has two subexpressions, e1 and e2. That leads to some choices about how to evaluate the expression:
二元运算符。二元运算符应用 e1 bop e2 有两个子表达式 e1e2 。这导致了关于如何评估表达式的一些选择:

  • We could first evaluate the left-hand side e1, then the right-hand side e2, then apply the operator.
    我们可以首先评估左侧 e1 ,然后评估右侧 e2 ,然后应用运算符。

  • Or we could do the right-hand side first, then the left-hand side.
    或者我们可以先做右侧,然后做左侧。

  • Or we could interleave the evaluation, first doing a step of e1, then of e2, then e1, then e2, etc.
    或者我们可以交错评估,首先执行 e1 的步骤,然后执行 e2 的步骤,然后执行 e1 的步骤,然后执行 e2 的步骤,等等。

  • Or maybe the operator is a short-circuit operator, in which case one of the subexpressions might never be evaluated.
    或者该运算符可能是短路运算符,在这种情况下,可能永远不会计算其中一个子表达式。

And there are many other strategies you might be able to invent.
您还可以发明许多其他策略。

It turns out that the OCaml language definition says that (for non-short-circuit operators) it is unspecified which side is evaluated first. The current implementation happens to evaluate the right-hand side first, but that’s not something any programmer should rely upon.
事实证明,OCaml 语言定义表示(对于非短路运算符)未指定先评估哪一侧。当前的实现恰好首先评估右侧,但这不是任何程序员都应该依赖的。

Many people would expect left-to-right evaluation, so let’s define the --> relation for that. We start by saying that the left-hand side can take a step:
许多人会期望从左到右的评估,所以让我们为此定义 --> 关系。我们首先说左边可以迈出一步:

e1 bop e2 --> e1' bop e2
  if e1 --> e1'

Similarly to the type system for SimPL, this rule says that two expressions are in the --> relation if two other (simpler) subexpressions are also in the --> relation. That’s what makes it an inductive definition.
与 SimPL 的类型系统类似,此规则表示,如果另外两个(更简单的)子表达式也在 --> 关系中,则两个表达式都在 --> 关系中。这就是它成为归纳定义的原因。

If the left-hand side is finished evaluating, then the right-hand side may begin stepping:
如果左侧完成评估,则右侧可以开始步进:

v1 bop e2 --> v1 bop e2'
  if e2 --> e2'

Finally, when both sides have reached a value, the binary operator may be applied:
最后,当双方都达到一个值时,可以应用二元运算符:

v1 bop v2 --> v
  if v is the result of primitive operation v1 bop v2

By primitive operation, we mean that there is some underlying notion of what bop actually means. For example, the character + is just a piece of syntax, but we are conditioned to understand its meaning as an arithmetic addition operation. The primitive operation typically is something implemented by hardware (e.g., an ADD opcode), or by a run-time library (e.g., a pow function).
通过原始操作,我们的意思是有一些关于 bop 实际含义的基本概念。例如,字符 + 只是一段语法,但我们有条件将其理解为算术加法运算。原语操作通常是由硬件(例如 ADD 操作码)或运行时库(例如 pow 函数)实现的。

For SimPL, let’s delegate all primitive operations to OCaml. That is, the SimPL + operator will be the same as the OCaml + operator, as will * and <=.
对于 SimPL,我们将所有原始操作委托给 OCaml。也就是说,SimPL + 运算符将与 OCaml + 运算符相同, *<= 也是如此。

Here’s an example of using the binary operator rule:
这是使用二元运算符规则的示例:

    (3*1000) + ((1*100) + ((1*10) + 0))
--> 3000 + ((1*100) + ((1*10) + 0))
--> 3000 + (100 + ((1*10) + 0))
--> 3000 + (100 + (10 + 0))
--> 3000 + (100 + 10)
--> 3000 + 110
--> 3110

If expressions. As with binary operators, there are many choices of how to evaluate the subexpressions of an if expression. Nonetheless, most programmers would expect the guard to be evaluated first, then only one of the branches to be evaluated, because that’s how most languages work. So let’s write evaluation rules for that semantics.
If 表达式。与二元运算符一样,对于如何计算 if 表达式的子表达式有多种选择。尽管如此,大多数程序员都希望首先评估防护,然后只评估其中一个分支,因为这就是大多数语言的工作方式。因此,让我们为该语义编写评估规则。

First, the guard is evaluated to a value:
首先,守卫被评估为一个值:

if e1 then e2 else e3 --> if e1' then e2 else e3
  if e1 --> e1'

Then, based on the guard, the if expression is simplified to just one of the branches:
然后,基于守卫,if 表达式被简化为仅分支之一:

if true then e2 else e3 --> e2

if false then e2 else e3 --> e3

Let expressions. Let’s make SimPL let expressions evaluate in the same way as OCaml let expressions: first the binding expression, then the body.
Let 表达式。让我们让 SimPL let 表达式以与 OCaml let 表达式相同的方式求值:首先是绑定表达式,然后是主体。

The rule that steps the binding expression is:
绑定表达式的步进规则是:

let x = e1 in e2 --> let x = e1' in e2
  if e1 --> e1'

Next, if the binding expression has reached a value, we want to substitute that value for the name of the variable in the body expression:
接下来,如果绑定表达式已达到某个值,我们希望将该值替换为主体表达式中的变量名称:

let x = v1 in e2 --> e2 with v1 substituted for x

For example, let x = 42 in x + 1 should step to 42 + 1, because substituting 42 for x in x + 1 yields 42 + 1.
例如, let x = 42 in x + 1 应步进到 42 + 1 ,因为在 x + 1 中用 42 替换 x 会产生 42 + 1

Of course, the right hand side of that rule isn’t really an expression. It’s just giving an intuition for the expression that we really want. We need to formally define what “substitute” means. It turns out to be rather tricky. So, rather then getting side-tracked by it right now, let’s assume a new notation: e'{e/x}, which means, “the expression e' with e substituted for x.” We’ll come back to that notation in the next section and give it a careful definition.
当然,该规则的右侧并不是真正的表达式。它只是为我们真正想要的表达提供一种直觉。我们需要正式定义“替代”的含义。事实证明这相当棘手。因此,与其现在被它转移注意力,不如假设一个新的符号: e'{e/x} ,这意味着“表达式 e'e 替换为 x ”。我们将在下一节中回到这个符号并给它一个仔细的定义。

For now, we can add this rule:
现在,我们可以添加这条规则:

let x = v1 in e2 --> e2{v1/x}

Variables. Note how the let expression rule eliminates a variable from showing up in the body expression: the variable’s name is replaced by the value that variable should have. So, we should never reach the point of attempting to step a variable name—assuming that the program was well typed.
变量。请注意 let 表达式规则如何消除变量在主体表达式中的显示:变量的名称被变量应具有的值替换。因此,我们永远不应该尝试单步执行变量名——假设程序的类型正确。

Consider OCaml: if we try to evaluate an expression with an unbound variable, what happens? Let’s check utop:
考虑 OCaml:如果我们尝试使用未绑定变量计算表达式,会发生什么?让我们检查一下utop:

# x;;
Error: Unbound value x

# let y = x in y;;
Error: Unbound value x

It’s an error —a type-checking error— for an expression to contain an unbound variable. Thus, any well-typed expression e will never reach the point of attempting to step a variable name.
对于包含未绑定变量的表达式来说,这是一个错误(类型检查错误)。因此,任何类型良好的表达式 e 永远不会达到尝试单步执行变量名称的程度。

As with constants, we therefore don’t need to add any rules for variables. But, for clarity, we could state that x -/->.
与常量一样,我们不需要为变量添加任何规则。但是,为了清楚起见,我们可以声明 x -/->

9.3.2. Implementing the Single-Step Relation
9.3.2. 实现单步关系 ¶

It’s easy to turn the above definitions of --> into an OCaml function that pattern matches against AST nodes. In the code below, recall that we have not yet finished defining substitution (i.e., subst); we’ll return to that in the next section.
可以很容易地将上面的 --> 定义转换为与 AST 节点进行模式匹配的 OCaml 函数。在下面的代码中,请记住我们尚未完成替换的定义(即 subst );我们将在下一节中回顾这一点。

(** [is_value e] is whether [e] is a value. *)
let is_value : expr -> bool = function
  | Int _ | Bool _ -> true
  | Var _ | Let _ | Binop _ | If _ -> false

(** [subst e v x] is [e{v/x}]. *)
let subst _ _ _ =
  failwith "See next section"

(** [step] is the [-->] relation, that is, a single step of
    evaluation. *)
let rec step : expr -> expr = function
  | Int _ | Bool _ -> failwith "Does not step"
  | Var _ -> failwith "Unbound variable"
  | Binop (bop, e1, e2) when is_value e1 && is_value e2 ->
    step_bop bop e1 e2
  | Binop (bop, e1, e2) when is_value e1 ->
    Binop (bop, e1, step e2)
  | Binop (bop, e1, e2) -> Binop (bop, step e1, e2)
  | Let (x, e1, e2) when is_value e1 -> subst e2 e1 x
  | Let (x, e1, e2) -> Let (x, step e1, e2)
  | If (Bool true, e2, _) -> e2
  | If (Bool false, _, e3) -> e3
  | If (Int _, _, _) -> failwith "Guard of if must have type bool"
  | If (e1, e2, e3) -> If (step e1, e2, e3)

(** [step_bop bop v1 v2] implements the primitive operation
    [v1 bop v2].  Requires: [v1] and [v2] are both values. *)
and step_bop bop e1 e2 = match bop, e1, e2 with
  | Add, Int a, Int b -> Int (a + b)
  | Mult, Int a, Int b -> Int (a * b)
  | Leq, Int a, Int b -> Bool (a <= b)
  | _ -> failwith "Operator and operand type mismatch"

The only new thing we had to deal with in that implementation was the two places where a run-time type error is discovered, namely, in the evaluation of If (Int _, _, _) and in the very last line, in which we discover that a binary operator is being applied to arguments of the wrong type. Type checking will guarantee that an exception never gets raised here, but OCaml’s exhaustiveness analysis of pattern matching forces us to write a branch nonetheless. Moreover, if it ever turned out that we had a bug in our type checker that caused ill-typed binary operator applications to be evaluated, this exception would help us discover what was going wrong.
在该实现中我们必须处理的唯一新问题是发现运行时类型错误的两个地方,即在 If (Int _, _, _) 的计算中和最后一行中,我们发现二元运算符应用于错误类型的参数。类型检查将保证这里永远不会引发异常,但 OCaml 对模式匹配的详尽分析仍然迫使我们编写一个分支。此外,如果我们的类型检查器中存在错误,导致对错误类型的二元运算符应用程序进行评估,则此异常将帮助我们发现问题所在。

9.3.3. The Multistep Relation
9.3.3. 多步关系 ¶

Now that we’ve defined -->, there’s really nothing left to do to define -->*. It’s just the reflexive transitive closure of -->. In other words, it can be defined with just these two rules:
现在我们已经定义了 --> ,实际上不需要再定义 -->* 了。这只是 --> 的自反传递闭包。换句话说,它可以只用这两个规则来定义:

e -->* e

e -->* e''
  if e --> e' and e' -->* e''

Of course, in implementing an interpreter, what we really want is to take as many steps as possible until the expression reaches a value. That is, we’re interested in the sub-relation e -->* v in which the right-hand side is a not just an expression, but a value. That’s easy to implement:
当然,在实现解释器时,我们真正想要的是采取尽可能多的步骤,直到表达式达到一个值。也就是说,我们对子关系 e -->* v 感兴趣,其中右侧不仅是表达式,而且是值。这很容易实现:

(** [eval_small e] is the [e -->* v] relation.  That is,
    keep applying [step] until a value is produced.  *)
let rec eval_small (e : expr) : expr =
  if is_value e then e
  else e |> step |> eval_small

9.3.4. Defining the Big-Step Relation
9.3.4. 定义大步关系 ¶

Recall that our goal in defining the big-step relation ==> is to make sure it agrees with the multistep relation -->*.
回想一下,我们定义大步关系 ==> 的目标是确保它与多步关系 -->* 一致。

Constants are easy, because they big-step to themselves:
常量很容易,因为它们向自己迈出了一大步:

i ==> i

b ==> b

Binary operators just big-step both of their subexpressions, then apply whatever the primitive operator is:
二元运算符只是将它们的两个子表达式向前迈一大步,然后应用原始运算符:

e1 bop e2 ==> v
  if e1 ==> v1
  and e2 ==> v2
  and v is the result of primitive operation v1 bop v2

If expressions big step the guard, then big step one of the branches:
如果表达式大步守卫,则大步分支之一:

if e1 then e2 else e3 ==> v2
  if e1 ==> true
  and e2 ==> v2

if e1 then e2 else e3 ==> v3
  if e1 ==> false
  and e3 ==> v3

Let expressions big step the binding expression, do a substitution, and big step the result of the substitution:
让表达式对绑定表达式进行大步操作,进行替换,然后对替换结果进行大步操作:

let x = e1 in e2 ==> v2
  if e1 ==> v1
  and e2{v1/x} ==> v2

Finally, variables do not big step, for the same reason as with the small step semantics—a well-typed program will never reach the point of attempting to evaluate a variable name:
最后,变量不会迈出大步,原因与小步语义相同——类型良好的程序永远不会达到尝试计算变量名称的程度:

x =/=>

9.3.5. Implementing the Big-Step Relation
9.3.5. 实现大步关系 ¶

The big-step evaluation relation is, if anything, even easier to implement than the small-step relation. It just recurses over the tree, evaluating subexpressions as required by the definition of ==>:
如果有的话,大步评估关系比小步评估关系更容易实现。它只是在树上递归,根据 ==> 定义的要求计算子表达式:

(** [eval_big e] is the [e ==> v] relation. *)
let rec eval_big (e : expr) : expr = match e with
  | Int _ | Bool _ -> e
  | Var _ -> failwith "Unbound variable"
  | Binop (bop, e1, e2) -> eval_bop bop e1 e2
  | Let (x, e1, e2) -> subst e2 (eval_big e1) x |> eval_big
  | If (e1, e2, e3) -> eval_if e1 e2 e3

(** [eval_bop bop e1 e2] is the [e] such that [e1 bop e2 ==> e]. *)
and eval_bop bop e1 e2 = match bop, eval_big e1, eval_big e2 with
  | Add, Int a, Int b -> Int (a + b)
  | Mult, Int a, Int b -> Int (a * b)
  | Leq, Int a, Int b -> Bool (a <= b)
  | _ -> failwith "Operator and operand type mismatch"

(** [eval_if e1 e2 e3] is the [e] such that [if e1 then e2 else e3 ==> e]. *)
and eval_if e1 e2 e3 = match eval_big e1 with
  | Bool true -> eval_big e2
  | Bool false -> eval_big e3
  | _ -> failwith "Guard of if must have type bool"

It’s good engineering practice to factor out functions for each of the pieces of syntax, as we did above, unless the implementation can fit on just a single line in the main pattern match inside eval_big.
正如我们上面所做的那样,为每个语法片段分解函数是一种很好的工程实践,除非实现只能适合 eval_big 内主模式匹配中的一行。

9.3.6. Substitution in SimPL
9.3.6. SimPL 中的替换 ¶

In the previous section, we posited a new notation e'{e/x}, meaning “the expression e' with e substituted for x.” The intuition is that anywhere x appears in e', we should replace x with e.
在上一节中,我们提出了一个新的符号 e'{e/x} ,意思是“用 e 替换 x 的表达式 e' ”。直觉是,无论 x 出现在 e' 中,我们都应该将 x 替换为 e

Let’s give a careful definition of substitution for SimPL. For the most part, it’s not too hard.
让我们仔细定义 SimPL 的替代。在大多数情况下,这并不太难。

Constants have no variables appearing in them (e.g., x cannot syntactically occur in 42), so substitution leaves them unchanged:
常量中没有出现变量(例如, x 在语法上不能出现在 42 中),因此替换会使它们保持不变:

i{e/x} = i
b{e/x} = b

For binary operators and if expressions, all that substitution needs to do is to recurse inside the subexpressions:
对于二元运算符和 if 表达式,替换所需要做的就是在子表达式内递归:

(e1 bop e2){e/x} = e1{e/x} bop e2{e/x}
(if e1 then e2 else e3){e/x} = if e1{e/x} then e2{e/x} else e3{e/x}

Variables start to get a little trickier. There are two possibilities: either we encounter the variable x, which means we should do the substitution, or we encounter some other variable with a different name, say y, in which case we should not do the substitution:
变量开始变得有点棘手。有两种可能性:要么我们遇到变量 x ,这意味着我们应该进行替换,或者我们遇到一些具有不同名称的其他变量,例如 y ,在这种情况下我们不应该做替换:

x{e/x} = e
y{e/x} = y

The first of those cases, x{e/x} = e, is important to note: it’s where the substitution operation finally takes place. Suppose, for example, we were trying to figure out the result of (x + 42){1/x}. Using the definitions from above,
第一种情况 x{e/x} = e 需要注意:这是替换操作最终发生的地方。例如,假设我们试图找出 (x + 42){1/x} 的结果。使用上面的定义,

  (x + 42){1/x}
= x{1/x} + 42{1/x}   by the bop case
= 1 + 42{1/x}        by the first variable case
= 1 + 42             by the integer case

Note that we are not defining the --> relation right now. That is, none of these equalities represents a step of evaluation. To make that concrete, suppose we were evaluating let x = 1 in x + 42:
请注意,我们现在没有定义 --> 关系。也就是说,这些等式都不代表评估步骤。为了具体说明,假设我们正在评估 let x = 1 in x + 42

    let x = 1 in x + 42
--> (x + 42){1/x}
  = 1 + 42
--> 43

There are two single steps here, one for the let and the other for +. But we consider the substitution to happen all at once, as part of the step that let takes. That’s why we write (x + 42){1/x} = 1 + 42, not (x + 42){1/x} --> 1 + 42.
这里有两个步骤,一个用于 let ,另一个用于 + 。但我们认为替换会同时发生,作为 let 所采取步骤的一部分。这就是为什么我们写 (x + 42){1/x} = 1 + 42 ,而不是 (x + 42){1/x} --> 1 + 42

Finally, let expressions also have two cases, depending on the name of the bound variable:
最后,let 表达式也有两种情况,具体取决于绑定变量的名称:

(let x = e1 in e2){e/x}  =  let x = e1{e/x} in e2
(let y = e1 in e2){e/x}  =  let y = e1{e/x} in e2{e/x}

Both of those cases substitute e for x inside the binding expression e1. That’s to ensure that expressions like let x = 42 in let y = x in y would evaluate correctly: x needs to be in scope inside the binding y = x, so we have to do a substitution there regardless of the name being bound.
这两种情况都将绑定表达式 e1 中的 x 替换为 e 。这是为了确保像 let x = 42 in let y = x in y 这样的表达式能够正确计算: x 需要在绑定 y = x 内的范围内,所以我们必须在那里进行替换,无论被绑定的名字。

But the first case does not do a substitution inside e2, whereas the second case does. That’s so we stop substituting when we reach a shadowed name. Consider let x = 5 in let x = 6 in x. We know it would evaluate to 6 in OCaml because of shadowing. Here’s how it would evaluate with our definitions of SimPL:
但第一种情况不会在 e2 内部进行替换,而第二种情况则进行替换。这样,当我们到达一个隐藏的名字时,我们就停止替换。考虑 let x = 5 in let x = 6 in x 。我们知道由于阴影,它在 OCaml 中的计算结果为 6 。以下是根据我们对 SimPL 的定义进行评估的方式:

    let x = 5 in let x = 6 in x
--> (let x = 6 in x){5/x}
  = let x = 6{5/x} in x      ***
  = let x = 6 in x
--> x{6/x}
  = 6

On the line tagged *** above, we’ve stopped substituting inside the body expression, because we reached a shadowed variable name. If we had instead kept going inside the body, we’d get a different result:
在上面标记为 *** 的行中,我们已停止在正文表达式内进行替换,因为我们到达了隐藏的变量名称。如果我们继续深入体内,我们会得到不同的结果:

    let x = 5 in let x = 6 in x
--> (let x = 6 in x){5/x}
  = let x = 6{5/x} in x{5/x}      ***WRONG***
  = let x = 6 in 5
--> 5{6/x}
  = 5

Example 1: 示例1:

let x = 2 in x + 1
--> (x + 1){2/x}
  = 2 + 1
--> 3

Example 2: 示例2:

    let x = 0 in (let x = 1 in x)
--> (let x = 1 in x){0/x}
  = (let x = 1{0/x} in x)
  = (let x = 1 in x)
--> x{1/x}
  = 1

Example 3: 示例3:

    let x = 0 in x + (let x = 1 in x)
--> (x + (let x = 1 in x)){0/x}
  = x{0/x} + (let x = 1 in x){0/x}
  = 0 + (let x = 1{0/x} in x)
  = 0 + (let x = 1 in x)
--> 0 + x{1/x}
  = 0 + 1
--> 1

9.3.7. Implementing Substitution
9.3.7. 实现取代 ¶

The definitions above are easy to turn into OCaml code. Note that, although we write v below, the function is actually able to substitute any expression for a variable, not just a value. The interpreter will only ever call this function on a value, though.
上面的定义很容易转换成 OCaml 代码。请注意,虽然我们在下面编写 v ,但该函数实际上可以用任何表达式替换变量,而不仅仅是值。不过,解释器只会对某个值调用此函数。

(** [subst e v x] is [e] with [v] substituted for [x], that
    is, [e{v/x}]. *)
let rec subst e v x = match e with
  | Var y -> if x = y then v else e
  | Bool _ -> e
  | Int _ -> e
  | Binop (bop, e1, e2) -> Binop (bop, subst e1 v x, subst e2 v x)
  | Let (y, e1, e2) ->
    let e1' = subst e1 v x in
    if x = y
    then Let (y, e1', e2)
    else Let (y, e1', subst e2 v x)
  | If (e1, e2, e3) ->
    If (subst e1 v x, subst e2 v x, subst e3 v x)

9.3.8. The SimPL Interpreter is Done!
9.3.8. SimPL 解释器已完成! ¶

We’ve completed developing our SimPL interpreter. Recall that the finished interpreter can be downloaded here: simpl.zip. It includes some rudimentary test cases, as well as makefile targets that you will find helpful.
我们已经完成了 SimPL 解释器的开发。回想一下,完成的解释器可以在这里下载:simpl.zip。它包括一些基本的测试用例,以及您会发现有用的 makefile 目标。

9.3.9. Capture-Avoiding Substitution
9.3.9. 捕获规避取代 ¶

The definition of substitution for SimPL was a little tricky but not too complicated. It turns out, though, that in general, the definition gets more complicated.
SimPL 替代的定义有点棘手,但并不太复杂。但事实证明,一般来说,定义变得更加复杂。

Let’s consider this tiny language:
让我们考虑一下这个小语言:

e ::= x | e1 e2 | fun x -> e
v ::= fun x -> e

This syntax is also known as the lambda calculus. There are only three kinds of expressions in it: variables, function application, and anonymous functions. The only values are anonymous functions. The language isn’t even typed. Yet, one of its most remarkable properties is that it is computationally universal: it can express any computable function. (To learn more about that, read about the Church-Turing Hypothesis.)
此语法也称为 lambda 演算。里面只有三种表达式:变量、函数应用、匿名函数。唯一的值是匿名函数。语言甚至都没有打字。然而,它最显着的特性之一是它在计算上是通用的:它可以表达任何可计算的函数。 (要了解更多信息,请阅读丘奇-图灵假说。)

There are several ways to define an evaluation semantics for the lambda calculus. Perhaps the simplest way—also closest to OCaml—uses the following rule:
有多种方法可以定义 lambda 演算的求值语义。也许最简单的方法(也最接近 OCaml)使用以下规则:

e1 e2 ==> v
  if e1 ==> fun x -> e
  and e2 ==> v2
  and e{v2/x} ==> v

This rule is the only rule we need: no other rules are required. This rule is also known as the call by value semantics, because it requires arguments to be reduced to values before a function can be applied. If that seems obvious, it’s because you’re used to it from OCaml.
这条规则是我们需要的唯一规则:不需要其他规则。此规则也称为按值调用语义,因为它要求在应用函数之前将参数还原为值。如果这看起来很明显,那是因为您已经习惯了 OCaml。

However, other semantics are certainly possible. For example, Haskell uses a variant called call by name, with the single rule:
然而,其他语义当然是可能的。例如,Haskell 使用名为 call by name 的变体,其规则如下:

e1 e2 ==> v
  if e1 ==> fun x -> e
  and e{e2/x} ==> v

With call by name, e2 does not have to be reduced to a value; that can lead to greater efficiency if the value of e2 is never needed.
通过名称调用, e2 不必简化为一个值;如果永远不需要 e2 的值,则可以提高效率。

Now we need to define the substitution operation for the lambda calculus. We’d like a definition that works for either call by name or call by value. Inspired by our definition for SimPL, here’s the beginning of a definition:
现在我们需要定义 lambda 演算的替换运算。我们想要一个适用于按名称调用或按值调用的定义。受我们对 SimPL 定义的启发,以下是定义的开头:

x{e/x} = e
y{e/x} = y
(e1 e2){e/x} = e1{e/x} e2{e/x}

The first two lines are exactly how we defined variable substitution in SimPL. The next line resembles how we defined binary operator substitution; we just recurse into the subexpressions.
前两行正是我们在 SimPL 中定义变量替换的方式。下一行类似于我们定义二元运算符替换的方式;我们只是递归到子表达式中。

What about substitution in a function? In SimPL, we stopped substituting when we reached a bound variable of the same name; otherwise, we proceeded. In the lambda calculus, that idea would be stated as follows:
函数中的替换怎么样?在 SimPL 中,当我们到达同名的绑定变量时,我们停止替换;否则,我们继续。在 lambda 演算中,这个想法可以表述如下:

(fun x -> e'){e/x} = fun x -> e'
(fun y -> e'){e/x} = fun y -> e'{e/x}

Perhaps surprisingly, that definition turns out to be incorrect. Here’s why: it violates the Principle of Name Irrelevance. Suppose we were attempting this substitution:
也许令人惊讶的是,这个定义被证明是错误的。原因如下:它违反了名称无关原则。假设我们正在尝试这种替换:

(fun z -> x){z/x}

The result would be:
结果将是:

  fun z -> x{z/x}
= fun z -> z

And, suddenly, a function that was not the identity function becomes the identity function. Whereas, if we had attempted this substitution:
突然间,一个不是恒等函数的函数就变成了恒等函数。然而,如果我们尝试这种替换:

(fun y -> x){z/x}

The result would be:
结果将是:

  fun y -> x{z/x}
= fun y -> z

Which is not the identity function. So our definition of substitution inside anonymous functions is incorrect, because it captures variables. A variable name being substituted inside an anonymous function can accidentally be “captured” by the function’s argument name.
这不是恒等函数。所以我们对匿名函数内部替换的定义是不正确的,因为它捕获了变量。在匿名函数内替换的变量名可能会意外地被函数的参数名称“捕获”。

Note that we never had this problem in SimPL, in part because SimPL was typed. The function fun y -> z if applied to any argument would just return z, which is an unbound variable. But the lambda calculus is untyped, so we can’t rely on typing to rule out this possibility here.
请注意,我们在 SimPL 中从未遇到过此问题,部分原因是 SimPL 是类型化的。函数 fun y -> z 如果应用于任何参数,只会返回 z ,这是一个未绑定的变量。但 lambda 演算是无类型的,因此我们不能依靠类型来排除这种可能性。

So the question becomes, how do we define substitution so that it gets the right answer, without capturing variables? The answer is called capture-avoiding substitution, and a correct definition of it eluded mathematicians for centuries.
所以问题就变成了,我们如何定义替换,以便在不捕获变量的情况下得到正确的答案?答案被称为“避免捕获替代”,几个世纪以来数学家一直没有给出正确的定义。

A correct definition is as follows:
正确的定义如下:

(fun x -> e'){e/x} = fun x -> e'
(fun y -> e'){e/x} = fun y -> e'{e/x}  if y is not in FV(e)

where FV(e) means the “free variables” of e, i.e., the variables that are not bound in it, and is defined as follows:
其中 FV(e) 表示 e 的“自由变量”,即未绑定在其中的变量,定义如下:

FV(x) = {x}
FV(e1 e2) = FV(e1) + FV(e2)
FV(fun x -> e) = FV(e) - {x}

and + means set union, and - means set difference.
+ 表示集合并, - 表示集合差。

That definition prevents the substitution (fun z -> x){z/x} from occurring, because z is in FV(z).
该定义防止发生替换 (fun z -> x){z/x} ,因为 z 位于 FV(z) 中。

Unfortunately, because of the side-condition y is not in FV(e), the substitution operation is now partial: there are times, like the example we just gave, where it cannot be applied.
不幸的是,由于附带条件 y is not in FV(e) ,替换操作现在是部分的:有时,就像我们刚刚给出的示例一样,它无法应用。

That problem can be solved by changing the names of variables: if we detect that a partiality has been encountered, we can change the name of the function’s argument. For example, when (fun z -> x){z/x} is encountered, the function’s argument could be replaced with a new name w that doesn’t occur anywhere else, yielding (fun w -> x){z/x}. (And if z occurred anywhere in the body, it would be replaced by w, too.) This is replacement, not substitution: absolutely anywhere we see z, we replace it with w. Then the substitution may proceed and correctly produce fun w -> z.
这个问题可以通过更改变量的名称来解决:如果我们检测到遇到了偏倚,我们可以更改函数参数的名称。例如,当遇到 (fun z -> x){z/x} 时,函数的参数可以替换为其他任何地方都不会出现的新名称 w ,从而产生 (fun w -> x){z/x} 。 (如果 z 出现在正文中的任何位置,它也会被 w 替换。)这是替换,而不是替换:绝对是我们看到 z 的任何地方,我们将其替换为 w 。然后替换可以继续并正确生成 fun w -> z

The tricky part of that is how to pick a new name that doesn’t occur anywhere else, that is, how to pick a fresh name. Here are three strategies:
其中棘手的部分是如何选择一个在其他地方没有出现过的新名字,即如何选择一个新鲜的名字。以下是三种策略:

  1. Pick a new variable name, check whether is fresh or not, and if not, try again, until that succeeds. For example, if trying to replace z, you might first try z', then z'', etc.
    选择一个新的变量名,检查是否新鲜,如果不是,请重试,直到成功。例如,如果尝试替换 z ,您可以首先尝试 z' ,然后 z'' 等。

  2. Augment the evaluation relation to maintain a stream (i.e., infinite list) of unused variable names. Each time you need a new one, take the head of the stream. But you have to be careful to use the tail of the stream anytime after that. To guarantee that they are unused, reserve some variable names for use by the interpreter alone, and make them illegal as variable names chosen by the programmer. For example, you might decide that programmer variable names may never start with the character $, then have a stream <$x1, $x2, $x3, ...> of fresh names.
    增强求值关系以维护未使用变量名称的流(即无限列表)。每次需要新的时,就拿走流的头部。但此后任何时候使用流的尾部都必须小心。为了保证它们不被使用,保留一些变量名供解释器单独使用,并让它们作为程序员选择的变量名是非法的。例如,您可能决定程序员变量名称永远不会以字符 $ 开头,然后有一个新名称流 <$x1, $x2, $x3, ...>

  3. Use an imperative counter to simulate the stream from the previous strategy. For example, the following function is guaranteed to return a fresh variable name each time it is called:
    使用命令式计数器来模拟先前策略中的流。例如,以下函数保证每次调用时返回一个新的变量名称:

    let gensym =
      let counter = ref 0 in
      fun () -> incr counter; "$x" ^ string_of_int !counter
    

    The name gensym is traditional for this kind of function. It comes from LISP, and shows up throughout compiler implementations. It means generate a fresh symbol.
    名称 gensym 是此类函数的传统名称。它来自 LISP,并出现在整个编译器实现中。这意味着生成一个新的符号。

There is a complete implementation of an interpreter for the lambda calculus, including capture-avoiding substitution, that you can download: lambda-subst.zip. It uses the gensym strategy from above to generate fresh names. There is a definition named strategy in main.ml that you can use to switch between call-by-value and call-by-name.
有一个完整的 lambda 演算解释器实现,包括避免捕获替换,您可以下载:lambda-subst.zip。它使用上面的 gensym 策略来生成新名称。 main.ml 中有一个名为 strategy 的定义,您可以使用它在按值调用和按名称调用之间切换。

9.3.10. Core OCaml 9.3.10. 核心 OCaml ¶

Let’s now upgrade from SimPL and the lambda calculus to a larger language that we call core OCaml. Here is its syntax in BNF:
现在让我们从 SimPL 和 lambda 演算升级到一种更大的语言,我们称之为核心 OCaml。以下是 BNF 格式的语法:

e ::= x | e1 e2 | fun x -> e
    | i | b | e1 bop e2
    | (e1, e2) | fst e | snd e
    | Left e | Right e
    | match e with Left x1 -> e1 | Right x2 -> e2
    | if e1 then e2 else e3
    | let x = e1 in e2

bop ::= + | * | < | =

x ::= <identifiers>

i ::= <integers>

b ::= true | false

v ::= fun x -> e | i | b | (v1, v2) | Left v | Right v

To keep tuples simple in this core model, we represent them with only two components (i.e., they are pairs). A longer tuple could be coded up with nested pairs. For example, (1, 2, 3) in OCaml could be (1, (2, 3)) in this core language.
为了使这个核心模型中的元组保持简单,我们仅用两个组件来表示它们(即它们是对)。更长的元组可以用嵌套对进行编码。例如,OCaml 中的 (1, 2, 3) 在此核心语言中可能是 (1, (2, 3))

Also to keep variant types simple in this core model, we represent them with only two constructors, which we name Left and Right. A variant with more constructors could be coded up with nested applications of those two constructors. Since we have only two constructors, match expressions need only two branches. One caution in reading the BNF above: the occurrence of | in the match expression just before the Right constructor denotes syntax, not metasyntax.
另外,为了保持核心模型中变体类型的简单性,我们仅使用两个构造函数来表示它们,我们将其命名为 LeftRight 。具有更多构造函数的变体可以使用这两个构造函数的嵌套应用程序进行编码。由于我们只有两个构造函数,因此匹配表达式只需要两个分支。阅读上面的 BNF 时需要注意一点:匹配表达式中 Right 构造函数之前出现的 | 表示语法,而不是元语法。

There are a few important OCaml constructs omitted from this core language, including recursive functions, exceptions, mutability, and modules. Types are also missing; core OCaml does not have any type checking. Nonetheless, there is enough in this core language to keep us entertained.
该核心语言中省略了一些重要的 OCaml 结构,包括递归函数、异常、可变性和模块。类型也缺失;核心 OCaml 没有任何类型检查。尽管如此,这个核心语言有足够的东西让我们开心。

9.3.11. Evaluating Core OCaml in the Substitution Model
9.3.11. 在替代模型中评估核心 OCaml ¶

Let’s define the small and big step relations for Core OCaml. To be honest, there won’t be much that’s surprising at this point; we’ve seen just about everything already in SimPL and in the lambda calculus.
让我们定义 Core OCaml 的小步和大步关系。老实说,此时此刻不会有太多令人惊讶的事情。我们已经在 SimPL 和 lambda 演算中看到了几乎所有内容。

Small-Step Relation. Here is the fragment of Core OCaml we already know from SimPL:
小步关系。下面是我们从 SimPL 中了解到的 Core OCaml 片段:

e1 + e2 --> e1' + e2
	if e1 --> e1'

v1 + e2 --> v1 + e2'
	if e2 --> e2'

i1 + i2 --> i3
	where i3 is the result of applying primitive operation +
	to i1 and i2

if e1 then e2 else e3 --> if e1' then e2 else e3
	if e1 --> e1'

if true then e2 else e3 --> e2

if false then e2 else e3 --> e3

let x = e1 in e2 --> let x = e1' in e2
	if e1 --> e1'

let x = v in e2 --> e2{v/x}

Here’s the fragment of Core OCaml that corresponds to the lambda calculus:
这是与 lambda 演算相对应的 Core OCaml 片段:

e1 e2 --> e1' e2
	if e1 --> e1'

v1 e2 --> v1 e2'
	if e2 --> e2'

(fun x -> e) v2 --> e{v2/x}

And here are the new parts of Core OCaml. First, pairs evaluate their first component, then their second component:
以下是 Core OCaml 的新部分。首先,两人一组评估其第一个组件,然后评估其第二个组件:

(e1, e2) --> (e1', e2)
	if e1 --> e1'

(v1, e2) --> (v1, e2')
	if e2 --> e2'

fst (v1, v2) --> v1

snd (v1, v2) --> v2

Constructors evaluate the expression they carry:
构造器评估它们携带的表达式:

Left e --> Left e'
	if e --> e'

Right e --> Right e'
	if e --> e'

Pattern matching evaluates the expression being matched, then reduces to one of the branches:
模式匹配评估正在匹配的表达式,然后简化为分支之一:

match e with Left x1 -> e1 | Right x2 -> e2
--> match e' with Left x1 -> e1 | Right x2 -> e2
	if e --> e'

match Left v with Left x1 -> e1 | Right x2 -> e2
--> e1{v/x1}

match Right v with Left x1 -> e1 | Right x2 -> e2
--> e2{v/x2}

Substitution. We also need to define the substitution operation for Core OCaml. Here is what we already know from SimPL and the lambda calculus:
代换。我们还需要为 Core OCaml 定义替换操作。以下是我们从 SimPL 和 lambda 演算中已知的信息:

i{v/x} = i

b{v/x} = b

(e1 + e2) {v/x} = e1{v/x} + e2{v/x}

(if e1 then e2 else e3){v/x}
 = if e1{v/x} then e2{v/x} else e3{v/x}

(let x = e1 in e2){v/x} = let x = e1{v/x} in e2

(let y = e1 in e2){v/x} = let y = e1{v/x} in e2{v/x}
  if y not in FV(v)

x{v/x} = v

y{v/x} = y

(e1 e2){v/x} = e1{v/x} e2{v/x}

(fun x -> e'){v/x} = (fun x -> e')

(fun y -> e'){v/x} = (fun y -> e'{v/x})
  if y not in FV(v)

Note that we’ve now added the requirement of capture-avoiding substitution to the definitions for let and fun: they both require y not to be in the free variables of v. We therefore need to define the free variables of an expression:
请注意,我们现在在 letfun 的定义中添加了避免捕获替换的要求:它们都要求 y 不能出现在 free 中。 v 的变量。因此,我们需要定义表达式的自由变量:

FV(x) = {x}
FV(e1 e2) = FV(e1) + FV(e2)
FV(fun x -> e) = FV(e) - {x}
FV(i) = {}
FV(b) = {}
FV(e1 bop e2) = FV(e1) + FV(e2)
FV((e1,e2)) = FV(e1) + FV(e2)
FV(fst e1) = FV(e1)
FV(snd e2) = FV(e2)
FV(Left e) = FV(e)
FV(Right e) = FV(e)
FV(match e with Left x1 -> e1 | Right x2 -> e2)
 = FV(e) + (FV(e1) - {x1}) + (FV(e2) - {x2})
FV(if e1 then e2 else e3) = FV(e1) + FV(e2) + FV(e3)
FV(let x = e1 in e2) = FV(e1) + (FV(e2) - {x})

Finally, we define substitution for the new syntactic forms in Core OCaml. Expressions that do not bind variables are easy to handle:
最后,我们定义了 Core OCaml 中新语法形式的替换。不绑定变量的表达式很容易处理:

(e1,e2){v/x} = (e1{v/x}, e2{v/x})

(fst e){v/x} = fst (e{v/x})

(snd e){v/x} = snd (e{v/x})

(Left e){v/x} = Left (e{v/x})

(Right e){v/x} = Right (e{v/x})

Match expressions take a little more work, just like let expressions and anonymous functions, to make sure we get capture-avoidance correct:
匹配表达式需要更多的工作,就像 let 表达式和匿名函数一样,以确保我们正确避免捕获:

(match e with Left x1 -> e1 | Right x2 -> e2){v/x}
 = match e{v/x} with Left x1 -> e1{v/x} | Right x2 -> e2{v/x}
     if ({x1,x2} intersect FV(v)) = {}

(match e with Left x -> e1 | Right x2 -> e2){v/x}
 = match e{v/x} with Left x -> e1 | Right x2 -> e2{v/x}
     if ({x2} intersect FV(v)) = {}

(match e with Left x1 -> e1 | Right x -> e2){v/x}
 = match e{v/x} with Left x1 -> e1{v/x} | Right x -> e2
      if ({x1} intersect FV(v)) = {}

(match e with Left x -> e1 | Right x -> e2){v/x}
 = match e{v/x} with Left x -> e1 | Right x -> e2

For typical implementations of programming languages, we don’t have to worry about capture-avoiding substitution because we only evaluate well-typed expressions, which don’t have free variables. But for more exotic programming languages, it can be necessary to evaluate open expressions. In these cases, we’d need all the extra conditions about free variables that we gave above.
对于编程语言的典型实现,我们不必担心避免捕获替换,因为我们只评估没有自由变量的类型良好的表达式。但对于更奇特的编程语言,可能有必要评估开放表达式。在这些情况下,我们需要上面给出的关于自由变量的所有额外条件。

9.3.12. Big-Step Relation
9.3.12. 大步关系 ¶

At this point there aren’t any new concepts remaining to introduce. We can just give the rules:
此时没有任何新概念需要介绍。我们可以给出规则:

e1 e2 ==> v
  if e1 ==> fun x -> e
  and e2 ==> v2
  and e{v2/x} ==> v

fun x -> e ==> fun x -> e

i ==> i

b ==> b

e1 bop e2 ==> v
  if e1 ==> v1
  and e2 ==> v2
  and v is the result of primitive operation v1 bop v2

(e1, e2) ==> (v1, v2)
  if e1 ==> v1
  and e2 ==> v2

fst e ==> v1
  if e ==> (v1, v2)

snd e ==> v2
  if e ==> (v1, v2)

Left e ==> Left v
  if e ==> v

Right e ==> Right v
  if e ==> v

match e with Left x1 -> e1 | Right x2 -> e2 ==> v
  if e ==> Left v1
  and e1{v1/x1} ==> v

match e with Left x1 -> e1 | Right x2 -> e2 ==> v
  if e ==> Right v2
  and e2{v2/x2} ==> v

if e1 then e2 else e3 ==> v
  if e1 ==> true
  and e2 ==> v

if e1 then e2 else e3 ==> v
  if e1 ==> false
  and e3 ==> v

let x = e1 in e2 ==> v
  if e1 ==> v1
  and e2{v1/x} ==> v

9.4. Environment Model 9.4. 环境模型 ¶

So far we’ve been using the substitution model to evaluate programs. It’s a great mental model for evaluation, and it’s commonly used in programming languages theory.
到目前为止,我们一直在使用替代模型来评估程序。这是一个很好的评估思维模型,并且常用于编程语言理论。

But when it comes to implementation, the substitution model is not the best choice. It’s too eager: it substitutes for every occurrence of a variable, even if that occurrence will never be needed. For example, let x = 42 in e will require crawling over all of e, which might be a very large expression, even if x never occurs in e, or even if x occurs only inside a branch of an if expression that never ends up being evaluated.
但在实施方面,替代模式并不是最佳选择。它太急切了:它替换了变量的每次出现,即使永远不需要该出现。例如, let x = 42 in e 将需要爬行所有 e ,这可能是一个非常大的表达式,即使 x 从未出现在 e ,或者即使 x 仅出现在永远不会被计算的 if 表达式的分支内。

For sake of efficiency, it would be better to substitute lazily: only when the value of a variable is needed should the interpreter have to do the substitution. That’s the key idea behind the environment model. In this model, there is a data structure called the dynamic environment, or just “environment” for short, that is a dictionary mapping variable names to values. Whenever the value of a variable is needed, it’s looked up in that dictionary.
为了提高效率,最好是惰性替换:只有当需要变量的值时,解释器才必须进行替换。这是环境模型背后的关键思想。在这个模型中,有一个称为动态环境的数据结构,简称“环境”,它是将变量名映射到值的字典。每当需要变量的值时,都会在该字典中查找它。

To account for the environment, the evaluation relation needs to change. Instead of e --> e' or e ==> v, both of which are binary relations, we now need a ternary relation, which is either
为了考虑到环境,评价关系需要改变。我们现在需要一个三元关系,而不是 e --> e'e ==> v ,它们都是二元关系

  • <env, e> --> e, or  <env, e> --> e ,或

  • <env, e> ==> v,  <env, e> ==> v

where env denotes the environment, and <env, e> is called a machine configuration. That configuration represents the state of the computer as it evaluates a program: env represents a part of the computer’s memory (the binding of variables to values), and e represents the program.
其中 env 表示环境, <env, e> 称为机器配置。该配置表示计算机在评估程序时的状态: env 表示计算机内存的一部分(变量与值的绑定), e 表示程序。

As notation, let: 作为符号,让:

  • {} represent the empty environment,
    {} 代表空环境,

  • {x1:v1, x2:v2, ...} represent the environment that binds x1 to v1, etc.,
    {x1:v1, x2:v2, ...} 表示将 x1 绑定到 v1 等的环境,

  • env[x -> v] represent the environment env with the variable x additionally bound to the value v, and
    env[x -> v] 表示环境 env ,其中变量 x 另外绑定到值 v ,并且

  • env(x) represent the binding of x in env.
    env(x) 表示 xenv 中的绑定。

If we wanted a more mathematical notation we would write instead of -> in env[x -> v], but we’re aiming for notation that is easily typed on a standard keyboard.
如果我们想要更数学的符号,我们会在 env[x -> v] 中编写 而不是 -> ,但我们的目标是在标准键盘上轻松键入的符号。

We’ll concentrate in the rest of this chapter on the big-step version of the environment model. It would of course be possible to define a small-step version, too.
我们将在本章的其余部分集中讨论环境模型的大步版本。当然也可以定义一个小步版本。

9.4.1. Evaluating the Lambda Calculus in the Environment Model
9.4.1. 在环境模型中评估 Lambda 演算 ¶

Recall that the lambda calculus is the fragment of a functional language involving functions and application:
回想一下,lambda 演算是涉及函数和应用程序的函数式语言的片段:

e ::= x | e1 e2 | fun x -> e

v ::= fun x -> e

Let’s explore how to define a big-step evaluation relation for the lambda calculus in the environment model. The rule for variables just says to look up the variable name in the environment:
我们来探讨一下如何为环境模型中的 lambda 演算定义大步求值关系。变量的规则只是说在环境中查找变量名称:

<env, x> ==> env(x)

This rule for functions says that an anonymous function evaluates just to itself. After all, functions are values:
函数的这条规则表明匿名函数仅计算其自身。毕竟,函数就是值:

<env, fun x -> e> ==> fun x -> e

Finally, this rule for application says to evaluate the left-hand side e1 to a function fun x -> e, the right-hand side to a value v2, then to evaluate the body e of the function in an extended environment that maps the function’s argument x to v2:
最后,此应用程序规则表示将左侧 e1 评估为函数 fun x -> e ,将右侧评估为值 v2 ,然后评估在将函数的参数 x 映射到 v2 的扩展环境中评估函数的主体 e

<env, e1 e2> ==> v
  if <env, e1> ==> fun x -> e
  and <env, e2> ==> v2
  and <env[x -> v2], e> ==> v

Seems reasonable, right? The problem is, it’s wrong. At least, it’s wrong if you want evaluation to behave the same as OCaml. Or, to be honest, nearly any other modern language.
看起来很合理,对吧?问题是,这是错误的。至少,如果您希望求值的行为与 OCaml 相同,那是错误的。或者,老实说,几乎任何其他现代语言都是。

It will be easier to explain why it’s wrong if we add two more language feature: let expressions and integer constants. Integer constants would evaluate to themselves:
如果我们再添加两个语言特性:let 表达式和整型常量,就会更容易解释为什么它是错误的。整数常量将对其自身求值:

<env, i> ==> i

As for let expressions, recall that we don’t actually need them, because let x = e1 in e2 can be rewritten as (fun x -> e2) e1. Nonetheless, their semantics would be:
至于 let 表达式,请记住我们实际上并不需要它们,因为 let x = e1 in e2 可以重写为 (fun x -> e2) e1 。尽管如此,它们的语义是:

<env, let x = e1 in e2> ==> v
  if <env, e1> ==> v1
  and <env[x -> v1], e2> ==> v

Which is a rule that really just follows from the other rules above, using that rewriting.
该规则实际上只是使用重写来遵循上述其他规则。

What would this expression evaluate to?
这个表达式的计算结果是什么?

let x = 1 in
let f = fun y -> x in
let x = 2 in
f 0

According to our semantics thus far, it would evaluate as follows:
根据我们迄今为止的语义,它将评估如下:

  • let x = 1 would produce the environment {x:1}.
    let x = 1 将产生环境 {x:1}

  • let f = fun y -> x would produce the environment {x:1, f:(fun y -> x)}.
    let f = fun y -> x 将产生环境 {x:1, f:(fun y -> x)}

  • let x = 2 would produce the environment {x:2, f:(fun y -> x)}. Note how the binding of x to 1 is shadowed by the new binding.
    let x = 2 将产生环境 {x:2, f:(fun y -> x)} 。请注意 x1 的绑定如何被新绑定隐藏。

  • Now we would evaluate <{x:2, f:(fun y -> x)}, f 0>:
    现在我们将评估 <{x:2, f:(fun y -> x)}, f 0>

    <{x:2, f:(fun y -> x)}, f 0> ==> 2
      because <{x:2, f:(fun y -> x)}, f> ==> fun y -> x
      and <{x:2, f:(fun y -> x)}, 0> ==> 0
      and <{x:2, f:(fun y -> x)}[y -> 0], x> ==> 2
        because <{x:2, f:(fun y -> x), y:0}, x> ==> 2
    
  • The result is therefore 2.
    因此结果是 2

But according to utop (and the substitution model), it evaluates as follows:
但根据 utop (以及替代模型),它的评估如下:

# let x = 1 in
  let f = fun y -> x in
  let x = 2 in
  f 0;;
- : int = 1

And the result is therefore 1. Obviously, 1 and 2 are different answers!
因此结果是 1 。显然, 12 是不同的答案!

What went wrong?? It has to do with scope.
什么地方出了错??这与范围有关。

9.4.2. Lexical vs. Dynamic Scope
9.4.2. 词法 vs 动态范围 ¶

There are two different ways to understand the scope of a variable: variables can be dynamically scoped or lexically scoped. It all comes down to the environment that is used when a function body is being evaluated:
有两种不同的方式来理解变量的作用域:变量可以是动态作用域的,也可以是词法作用域的。这一切都取决于评估函数体时使用的环境:

  • With the rule of dynamic scope, the body of a function is evaluated in the current dynamic environment at the time the function is applied, not the old dynamic environment that existed at the time the function was defined.
    根据动态作用域规则,函数体是在应用函数时的当前动态环境中计算的,而不是在定义函数时存在的旧动态环境中计算的。

  • With the rule of lexical scope, the body of a function is evaluated in the old dynamic environment that existed at the time the function was defined, not the current environment when the function is applied.
    根据词法作用域的规则,函数体是在定义函数时存在的旧动态环境中计算的,而不是在应用函数时的当前环境中计算的。

The rule of dynamic scope is what our semantics, above, implemented. Let’s look back at the semantics of function application:
动态作用域的规则就是我们上面的语义所实现的。我们回顾一下函数应用的语义:

<env, e1 e2> ==> v
  if <env, e1> ==> fun x -> e
  and <env, e2> ==> v2
  and <env[x -> v2], e> ==> v

Note how the body e is being evaluated in the same environment env as when the function is applied. In the example program
请注意主体 e 是如何在与应用函数时相同的环境 env 中进行评估的。在示例程序中

let x = 1 in
let f = fun y -> x in
let x = 2 in
f 0

that means that f is evaluated in an environment in which x is bound to 2, because that’s the most recent binding of x.
这意味着 fx 绑定到 2 的环境中进行评估,因为这是 x 的最新绑定。

But OCaml implements the rule of lexical scope, which coincides with the substitution model. With that rule, x is bound to 1 in the body of f when f is defined, and the later binding of x to 2 doesn’t change that fact.
但 OCaml 实现了词法作用域规则,这与替换模型不谋而合。根据该规则,当定义 f 时, x 绑定到 f 主体中的 1 ,并且稍后绑定 x2 并没有改变这个事实。

The consensus after decades of experience with programming language design is that lexical scope is the right choice. Perhaps the main reason for that is that lexical scope supports the Principle of Name Irrelevance. Recall, that principle says that the name of a variable shouldn’t matter to the meaning of program, as long as the name is used consistently.
经过几十年的编程语言设计经验,大家一致认为词法作用域是正确的选择。也许主要原因是词法作用域支持名称无关原则。回想一下,该原则说变量的名称与程序的含义无关,只要名称的使用一致即可。

Nonetheless, dynamic scope is useful in some situations. Some languages use it as the norm (e.g., Emacs LISP, LaTeX), and some languages have special ways to do it (e.g., Perl, Racket). But these days, most languages just don’t have it.
尽管如此,动态作用域在某些情况下还是有用的。有些语言使用它作为规范(例如,Emacs LISP、LaTeX),而有些语言则有特殊的方法(例如,Perl、Racket)。但如今,大多数语言都没有它。

There is one language feature that modern languages do have that resembles dynamic scope, and that is exceptions. Exception handling resembles dynamic scope, in that raising an exception transfers control to the “most recent” exception handler, just like how dynamic scope uses the “most recent” binding of variable.
现代语言确实具有一种类似于动态作用域的语言功能,那就是例外。异常处理类似于动态作用域,因为引发异常会将控制权转移到“最新”异常处理程序,就像动态作用域如何使用“最新”变量绑定一样。

9.4.3. A Second Attempt at Evaluating the Lambda Calculus in the Environment Model
9.4.3. 在环境模型中评估 Lambda 演算的第二次尝试 ¶

The question then becomes, how do we implement lexical scope? It seems to require time travel, because function bodies need to be evaluated in old dynamic environment that have long since disappeared.
那么问题就变成了,我们如何实现词法作用域?这似乎需要时间旅行,因为函数体需要在早已消失的旧动态环境中进行评估。

The answer is that the language implementation must arrange to keep old environments around. And that is indeed what OCaml and other languages must do. They use a data structure called a closure for this purpose.
答案是语言实现必须安排保留旧环境。这确实是 OCaml 和其他语言必须做的事情。为此,他们使用一种称为闭包的数据结构。

A closure has two parts:
闭包有两个部分:

  • a code part, which contains a function fun x -> e, and
    代码部分,其中包含函数 fun x -> e ,以及

  • an environment part, which contains the environment env at the time that function was defined.
    环境部分,其中包含定义函数时的环境 env

You can think of a closure as being like a pair, except that there’s no way to directly write a closure in OCaml source code, and there’s no way to destruct the pair into its components in OCaml source code. The pair is entirely hidden from you by the language implementation.
您可以将闭包视为一对,只不过无法在 OCaml 源代码中直接编写闭包,并且无法在 OCaml 源代码中将对解构为其组件。语言实现对您完全隐藏了该对。

Let’s notate a closure as (| fun x -> e, env |). The delimiters (| ... |) are meant to evoke an OCaml pair, but of course they are not legal OCaml syntax.
让我们将闭包标记为 (| fun x -> e, env |) 。分隔符 (| ... |) 旨在唤起 OCaml 对,但它们当然不是合法的 OCaml 语法。

Using that notation, we can re-define the evaluation relation as follows:
使用该符号,我们可以重新定义评估关系,如下所示:

The rule for functions now says that an anonymous function evaluates to a closure:
函数的规则现在规定匿名函数的计算结果为闭包:

<env, fun x -> e> ==> (| fun x -> e, env |)

That rule saves the defining environment as part of the closure, so that it can be used at some future point.
该规则将定义环境保存为闭包的一部分,以便可以在将来的某个时刻使用它。

The rule for application says to use that closure:
应用程序规则要求使用该闭包:

<env, e1 e2> ==> v
  if <env, e1> ==> (| fun x -> e, defenv |)
  and <env, e2> ==> v2
  and <defenv[x -> v2], e> ==> v

That rule uses the closure’s environment defenv (whose name is meant to suggest the “defining environment”) to evaluate the function body e.
该规则使用闭包的环境 defenv (其名称表示“定义环境”)来评估函数体 e

The derived rule for let expressions remains unchanged:
let 表达式的派生规则保持不变:

<env, let x = e1 in e2> ==> v
  if <env, e1> ==> v1
  and <env[x -> v1], e2> ==> v

That’s because the defining environment for the body e2 is the same as the current environment env when the let expression is being evaluated.
这是因为在评估 let 表达式时,主体 e2 的定义环境与当前环境 env 相同。

9.4.4. An Implementation of SimPL in the Environment Model
9.4.4. SimPL 在环境模型中的实现 ¶

You can download a complete implementation of the two semantics above: lambda-env.zip In main.ml, there is a definition named scope that you can use to switch between lexical and dynamic scope.
您可以下载上述两种语义的完整实现:​​ lambda-env.zip 在 main.ml 中,有一个名为 scope 的定义,您可以使用它在词法作用域和动态作用域之间切换。

9.4.5. Evaluating Core OCaml in the Environment Model
9.4.5. 在环境模型中评估核心 OCaml ¶

There isn’t anything new in the (big step) environment model semantics of Core OCaml, now that we know about closures, but for sake of completeness let’s state it anyway.
既然我们已经了解了闭包,那么 Core OCaml 的(大步)环境模型语义中并没有任何新内容,但为了完整起见,我们还是要说明一下。

Syntax. 语法。

e ::= x | e1 e2 | fun x -> e
    | i | b | e1 + e2
    | (e1,e2) | fst e1 | snd e2
    | Left e | Right e
    | match e with Left x1 -> e1 | Right x2 -> e2
    | if e1 then e2 else e3
    | let x = e1 in e2

Semantics. 语义。

We’ve already seen the semantics of the lambda calculus fragment of Core OCaml:
我们已经了解了 Core OCaml 的 lambda 演算片段的语义:

<env, x> ==> v
  if env(x) = v

<env, e1 e2> ==> v
  if  <env, e1> ==> (| fun x -> e, defenv |)
  and <env, e2> ==> v2
  and <defenv[x -> v2], e> ==> v

<env, fun x -> e> ==> (|fun x -> e, env|)

Evaluation of constants ignores the environment:
常量的计算忽略了环境:

<env, i> ==> i

<env, b> ==> b

Evaluation of most other language features just uses the environment without changing it:
大多数其他语言功能的评估只是使用环境而不改变它:

<env, e1 + e2> ==> n
  if  <env,e1> ==> n1
  and <env,e2> ==> n2
  and n is the result of applying the primitive operation + to n1 and n2

<env, (e1, e2)> ==> (v1, v2)
  if  <env, e1> ==> v1
  and <env, e2> ==> v2

<env, fst e> ==> v1
  if <env, e> ==> (v1, v2)

<env, snd e> ==> v2
  if <env, e> ==> (v1, v2)

<env, Left e> ==> Left v
  if <env, e> ==> v

<env, Right e> ==> Right v
  if <env, e> ==> v

<env, if e1 then e2 else e3> ==> v2
  if <env, e1> ==> true
  and <env, e2> ==> v2

<env, if e1 then e2 else e3> ==> v3
  if <env, e1> ==> false
  and <env, e3> ==> v3

Finally, evaluation of binding constructs (i.e., match and let expression) extends the environment with a new binding:
最后,对绑定构造(即 match 和 let 表达式)的评估使用新的绑定扩展了环境:

<env, match e with Left x1 -> e1 | Right x2 -> e2> ==> v1
  if  <env, e> ==> Left v
  and <env[x1 -> v], e1> ==> v1

<env, match e with Left x1 -> e1 | Right x2 -> e2> ==> v2
  if  <env, e> ==> Right v
  and <env[x2 -> v], e2> ==> v2

<env, let x = e1 in e2> ==> v2
  if  <env, e1> ==> v1
  and <env[x -> v1], e2> ==> v2

9.5. Type Checking 9.5. 类型检查 ¶

Earlier, we skipped over the type checking phase. Let’s come back to that now. After lexing and parsing, the next phase of compilation is semantic analysis, and the primary task of semantic analysis is type checking.
早些时候,我们跳过了类型检查阶段。现在让我们回到这一点。在词法分析和解析之后,编译的下一阶段是语义分析,而语义分析的首要任务是类型检查。

A type system is a mathematical description of how to determine whether an expression is ill typed or well typed, and in the latter case, what the type of the expression is. A type checker is a program that implements a type system, i.e., that implements the static semantics of the language.
类型系统是如何确定表达式类型错误或类型良好以及在后一种情况下表达式的类型是什么的数学描述。类型检查器是实现类型系统的程序,即实现语言的静态语义。

Commonly, a type system is formulated as a ternary relation HasType(Γ,e,t), which means that expression e has type t in static environment Γ. A static environment, aka typing context, is a map from identifiers to types. The static environment is used to record what variables are in scope, and what their types are. The use of the Greek letter Γ for static environments is traditional.
通常,类型系统被表述为三元关系 HasType(Γ,e,t) ,这意味着表达式 e 在静态环境 Γ 中具有类型 t 。静态环境,又名打字上下文,是从标识符到类型的映射。静态环境用于记录哪些变量在作用域内以及它们的类型是什么。静态环境使用希腊字母 Γ 是传统做法。

That ternary relation HasType is typically written with infix notation, though, as Γe:t. You can read the turnstile symbol as “proves” or “shows”, i.e., the static environment Γ shows that e has type t.
不过,三元关系 HasType 通常用中缀表示法编写为 Γe:t 。您可以将十字转门符号 解读为“证明”或“显示”,即静态环境 Γ 显示 e 具有类型 t

Let’s make that notation a little friendlier by eliminating the Greek and the math typesetting. We’ll just write env |- e : t to mean that static environment env shows that e has type t. We previously used env to mean a dynamic environment in the big-step relation ==>. Since it’s always possible to see whether we’re using the ==> or |- relation, the meaning of env as either a dynamic or static environment is always discernible.
让我们通过消除希腊语和数学排版来使这种符号更友好一些。我们只写 env |- e : t 来表示静态环境 env 显示 e 具有类型 t 。我们之前使用 env 来表示大步关系 ==> 中的动态环境。由于总是可以看出我们使用的是 ==> 还是 |- 关系,因此 env 作为动态或静态环境的含义始终是可辨别的。

Let’s write {} for the empty static environment, and x:t to mean that x is bound to t. So, {foo:int, bar:bool} would be the static environment is which foo has type int and bar has type bool. A static environment may bind an identifier at most once. We’ll write env[x -> t] to mean a static environment that contains all the bindings of env, and also binds x to t. If x was already bound in env, then that old binding is replaced by the new binding to t in env[x -> t]. As with dynamic environments, if we wanted a more mathematical notation we would write instead of -> in env[x -> v], but we’re aiming for notation that is easily typed on a standard keyboard.
让我们为空静态环境编写 {} ,并编写 x:t 表示 x 绑定到 t 。因此, {foo:int, bar:bool} 将是静态环境,其中 foo 具有类型 intbar 具有类型 bool 。静态环境最多可以绑定一个标识符一次。我们将编写 env[x -> t] 来表示包含 env 的所有绑定的静态环境,并将 x 绑定到 t 。如果 x 已绑定在 env 中,则旧绑定将替换为 env[x -> t]t 的新绑定。与动态环境一样,如果我们想要更数学的表示法,我们会在 env[x -> v] 中编写 而不是 -> ,但我们的目标是易于使用的表示法在标准键盘上输入。

With all that machinery, we can at last define what it means to be well typed: An expression e is well typed in static environment env if there exists a type t for which env |- e : t. The goal of a type checker is thus to find such a type t, starting from some initial static environment.
有了所有这些机制,我们最终可以定义什么是类型正确:一个表达式 e 在静态环境 env 中将会是类型良好的,如果存在类型 t 对于 env |- e : t 这一形式。因此,类型检查器的目标是从某些初始静态环境开始找到这样的类型 t

It’s convenient to pretend that the initial static environment is empty. But in practice, it’s rare that a language truly uses the empty static environment to determine whether a program is well typed. In OCaml, for example, there are many built-in identifiers that are always in scope, such as everything in the Stdlib module.
假装初始静态环境是空的很方便。但在实践中,很少有语言真正使用空静态环境来确定程序是否类型良好。例如,在 OCaml 中,有许多始终在范围内的内置标识符,例如 Stdlib 模块中的所有内容。

9.5.1. A Type System for SimPL
9.5.1. SimPL 的类型系统 ¶

Recall the syntax of SimPL:
回想一下 SimPL 的语法:

e ::= x | i | b | e1 bop e2
    | if e1 then e2 else e3
    | let x = e1 in e2

bop ::= + | * | <=

Let’s define a type system env |- e : t for SimPL. The only types in SimPL are integers and booleans:
让我们为 SimPL 定义一个类型系统 env |- e : t 。 SimPL 中唯一的类型是整数和布尔值:

t ::= int | bool

To define |-, we’ll invent a set of typing rules that specify what the type of an expression is based on the types of its subexpressions. In other words, |- is an inductively-defined relation, as can be learned about in a discrete math course. So, it has some base cases, and some inductive cases.
为了定义 |- ,我们将发明一组类型规则,根据表达式的子表达式的类型来指定表达式的类型。换句话说, |- 是一个归纳定义的关系,可以在离散数学课程中了解到。因此,它有一些基本情况和一些归纳情况。

For the base cases, an integer constant has type int in any static environment whatsoever, a Boolean constant likewise always has type bool, and a variable has whatever type the static environment says it should have. Here are the typing rules that express those ideas:
对于基本情况,整数常量在任何静态环境中都具有类型 int ,布尔常量同样始终具有类型 bool ,变量具有静态环境规定的任何类型有。以下是表达这些想法的打字规则:

env |- i : int
env |- b : bool
{x : t, ...} |- x : t

The remaining syntactic forms are inductive cases.
其余的句法形式是归纳格。

Let. As we already know from OCaml, we type check the body of a let expression using a scope that is extended with a new binding.
Let。正如我们从 OCaml 中了解到的,我们使用通过新绑定扩展的作用域对 let 表达式的主体进行类型检查。

env |- let x = e1 in e2 : t2
  if env |- e1 : t1
  and env[x -> t1] |- e2 : t2

The rule says that let x = e1 in e2 has type t2 in static environment env, but only if certain conditions hold. The first condition is that e1 has type t1 in env. The second is that e2 has type t2 in a new static environment, which is env extended to bind x to t1.
该规则规定 let x = e1 in e2 在静态环境 env 中具有类型 t2 ,但前提是满足某些条件。第一个条件是 e1env 中具有类型 t1 。第二个是 e2 在新的静态环境中具有类型 t2 ,它被扩展为 envx 绑定到 t1

Binary operators. We’ll need a couple different rules for binary operators.
二元运算符。我们需要一些不同的二元运算符规则。

env |- e1 bop e2 : int
  if bop is + or *
  and env |- e1 : int
  and env |- e2 : int

env |- e1 <= e2 : bool
  if env |- e1 : int
  and env |- e2 : int

If. Just like OCaml, an if expression must have a Boolean guard, and its two branches must have the same type.
If。就像 OCaml 一样,if 表达式必须有布尔保护,并且它的两个分支必须具有相同的类型。

env |- if e1 then e2 else e3 : t
  if env |- e1 : bool
  and env |- e2 : t
  and env |- e3 : t

9.5.2. A Type Checker for SimPL
9.5.2. SimPL 的类型检查器 ¶

Let’s implement a type checker for SimPL, based on the type system we defined in the previous section. You can download the completed type checker as part of the SimPL interpreter: simpl.zip
让我们基于上一节中定义的类型系统为 SimPL 实现一个类型检查器。您可以下载完整的类型检查器作为 SimPL 解释器的一部分:simpl.zip

We need a variant to represent types:
我们需要一个变体来表示类型:

type typ =
  | TInt
  | TBool

The natural name for that variant would of course have been “type” not “typ”, but the former is already a keyword in OCaml. We have to prefix the constructors with “T” to disambiguate them from the constructors of the expr type, which include Int and Bool.
该变体的自然名称当然是“type”而不是“typ”,但前者已经是 OCaml 中的关键字。我们必须在构造函数前面加上“T”前缀,以消除它们与 expr 类型的构造函数的歧义,其中包括 IntBool

Let’s introduce a small signature for static environments, based on the abstractions we’ve introduced so far: the empty static environment, looking up a variable, and extending a static environment.
让我们根据目前为止介绍的抽象来介绍静态环境的一个小签名:空静态环境、查找变量以及扩展静态环境。

module type StaticEnvironment = sig
  (** [t] is the type of a static environment. *)
  type t

  (** [empty] is the empty static environment. *)
  val empty : t

  (** [lookup env x] gets the binding of [x] in [env].
      Raises: [Failure] if [x] is not bound in [env]. *)
  val lookup : t -> string -> typ

  (** [extend env x ty] is [env] extended with a binding
      of [x] to [ty]. *)
  val extend : t -> string -> typ -> t
end

It’s easy to implement that signature with an association list.
使用关联列表很容易实现该签名。

module StaticEnvironment : StaticEnvironment = struct
  type t = (string * typ) list

  let empty = []

  let lookup env x =
    try List.assoc x env
    with Not_found -> failwith "Unbound variable"

  let extend env x ty =
    (x, ty) :: env
end

Now we can implement the typing relation |-. We’ll do that by writing a function typeof : StaticEnvironment.t -> expr -> typ, such that typeof env e = t if and only if env |- e : t. Note that the typeof function produces the type as output, so the function is actually inferring the type! That inference is easy for SimPL; it would be considerably harder for larger languages.
现在我们可以实现类型关系 |- 。我们将通过编写一个函数 typeof : StaticEnvironment.t -> expr -> typ 来做到这一点,这样 typeof env e = t 当且仅当 env |- e : t 。请注意, typeof 函数生成类型作为输出,因此该函数实际上是在推断类型!对于 SimPL 来说,这一推论很容易;对于较大的语言来说,这会困难得多。

Let’s start with the base cases:
让我们从基本案例开始:

open StaticEnvironment

(** [typeof env e] is the type of [e] in static environment [env].
    Raises: [Failure] if [e] is not well typed in [env]. *)
let rec typeof env = function
  | Int _ -> TInt
  | Bool _ -> TBool
  | Var x -> lookup env x
  ...

Note how the implementation of typeof so far is based on the rules we previously defined for |-. In particular:
请注意到目前为止 typeof 的实现是如何基于我们之前为 |- 定义的规则的。尤其:

  • typeof is a recursive function, just as |- is an inductive relation.
    typeof 是一个递归函数,就像 |- 是一个归纳关系一样。

  • The base cases for the recursion of typeof are the same as the base cases for |-.
    typeof 递归的基本情况与 |- 的基本情况相同。

Also note how the implementation of typeof differs in one major way from the definition of |-: error handling. The type system didn’t say what to do about errors; rather, it just defined what it meant to be well typed. The type checker, on the other hand, needs to take action and report ill typed programs. Our typeof function does that by raising exceptions. The lookup function, in particular, will raise an exception if we attempt to lookup a variable that hasn’t been bound in the static environment.
另请注意 typeof 的实现与 |- 的定义的一个主要区别:错误处理。类型系统没有说明如何处理错误;相反,它只是定义了良好类型的含义。另一方面,类型检查器需要采取行动并报告类型错误的程序。我们的 typeof 函数通过引发异常来实现这一点。特别是,如果我们尝试查找尚未在静态环境中绑定的变量,则 lookup 函数将引发异常。

Let’s continue with the recursive cases:
让我们继续讨论递归情况:

  ...
  | Let (x, e1, e2) -> typeof_let env x e1 e2
  | Binop (bop, e1, e2) -> typeof_bop env bop e1 e2
  | If (e1, e2, e3) -> typeof_if env e1 e2 e3

We’re factoring out a helper function for each branch for the sake of keeping the pattern match readable. Each of the helpers directly encodes the ideas of the |- rules, with error handling added.
为了保持模式匹配的可读性,我们为每个分支分解了一个辅助函数。每个帮助器都直接编码 |- 规则的思想,并添加了错误处理。

and typeof_let env x e1 e2 =
  let t1 = typeof env e1 in
  let env' = extend env x t1 in
  typeof env' e2

and typeof_bop env bop e1 e2 =
  let t1, t2 = typeof env e1, typeof env e2 in
  match bop, t1, t2 with
  | Add, TInt, TInt
  | Mult, TInt, TInt -> TInt
  | Leq, TInt, TInt -> TBool
  | _ -> failwith "Operator and operand type mismatch"

and typeof_if env e1 e2 e3 =
  if typeof env e1 = TBool
  then begin
    let t2 = typeof env e2 in
    if t2 = typeof env e3 then t2
    else failwith "Branches of if must have same type"
  end
  else failwith "Guard of if must have type bool"

Note how the recursive calls in the implementation of typeof occur exactly in the same places where the definition of |- is inductive.
请注意 typeof 实现中的递归调用如何恰好发生在 |- 的归纳定义的相同位置。

Finally, we can implement a function to check whether an expression is well typed:
最后,我们可以实现一个函数来检查表达式是否类型正确:

(** [typecheck e] checks whether [e] is well typed in
    the empty static environment. Raises: [Failure] if not. *)
let typecheck e =
  ignore (typeof empty e)

9.5.3. Type Safety 9.5.3. 类型安全 ¶

What is the purpose of a type system? There might be many, but one of the primary purposes is to ensure that certain run-time errors don’t occur. Now that we know how to formalize type systems with the |- relation and evaluation with the --> relation, we can make that idea precise.
类型系统的目的是什么?可能有很多,但主要目的之一是确保不会发生某些运行时错误。现在我们知道如何使用 |- 关系形式化类型系统并使用 --> 关系进行评估,我们可以使这个想法更加精确。

The goals of a language designer usually include ensuring that these two properties, which establish a relationship between |- and -->, both hold:
语言设计者的目标通常包括确保在 |---> 之间建立关系的这两个属性都成立:

  • Progress: If an expression is well typed, then either it is already a value, or it can take at least one step. We can formalize that as, “for all e, if there exists a t such that {} |- e : t, then e is a value, or there exists an e' such that e --> e'.”
    进度:如果表达式的类型正确,那么它要么已经是一个值,要么可以至少执行一步。我们可以将其形式化为,“对于所有 e ,如果存在 t 使得 {} |- e : t ,那么 e 是一个值,或者存在 e' 使得 e --> e' 。”

  • Preservation: If an expression is well typed, then if the expression steps, the new expression has the same type as the old expression. Formally, “for all e and t such that {} |- e : t, if there exists an e' such that e --> e', then {} |- e' : t.”
    保留:如果表达式类型正确,那么如果表达式步进,新表达式与旧表达式具有相同的类型。形式上,“对于所有 et 使得 {} |- e : t ,如果存在 e' 使得 e --> e' ,然后 {} |- e' : t 。”

Put together, progress plus preservation imply that evaluation of a well-typed expression can never get stuck, meaning it reaches a non-value that cannot take a step. This property is known as type safety.
总而言之,进度加上保存意味着对类型正确的表达式的求值永远不会被卡住,这意味着它达到了无法迈出一步的非值。此属性称为类型安全。

For example, 5 + true would get stuck using the SimPL evaluation relation, because the primitive + operation cannot accept a Boolean as an operand. But the SimPL type system won’t accept that program, thus saving us from ever reaching that situation.
例如, 5 + true 使用 SimPL 求值关系会陷入困境,因为原始 + 操作无法接受布尔值作为操作数。但 SimPL 类型系统不会接受该程序,从而使我们避免遇到这种情况。

Looking back at the SimPL we wrote, everywhere in the implementation of step where we raised an exception was a place where evaluation would get stuck. But the type system guarantees those exceptions will never occur.
回顾我们编写的 SimPL,在 step 的实现中,凡是引发异常的地方都是评估会卡住的地方。但类型系统保证这些异常永远不会发生。

9.6. Type Inference 9.6. 类型推断 ¶

OCaml and Java are statically typed languages, meaning every binding has a type that is determined at compile time—that is, before any part of the program is executed. The type-checker is a compile-time procedure that either accepts or rejects a program. By contrast, JavaScript and Ruby are dynamically-typed languages; the type of a binding is not determined ahead of time. Computations like binding 42 to x and then treating x as a string therefore either result in run-time errors, or run-time conversion between types.
OCaml 和 Java 是静态类型语言,这意味着每个绑定都有一个在编译时(即在执行程序的任何部分之前)确定的类型。类型检查器是一个编译时过程,它接受或拒绝程序。相比之下,JavaScript 和 Ruby 是动态类型语言;绑定的类型不是提前确定的。因此,像将 42 绑定到 x 然后将 x 视为字符串这样的计算要么会导致运行时错误,要么会导致类型之间的运行时转换。

Unlike Java, OCaml is implicitly typed, meaning programmers rarely need to write down the types of bindings. This is often convenient, especially with higher-order functions. (Although some people disagree as to whether it makes code easier or harder to read). But implicit typing in no way changes the fact that OCaml is statically typed. Rather, the type-checker has to be more sophisticated because it must infer what the type annotations “would have been” had the programmers written all of them. In principle, type inference and type checking could be separate procedures (the inferencer could figure out the types then the checker could determine whether the program is well-typed), but in practice they are often merged into a single procedure called type reconstruction.
与 Java 不同,OCaml 是隐式类型的,这意味着程序员很少需要写下绑定的类型。这通常很方便,尤其是对于高阶函数。 (尽管有些人不同意它是否使代码更容易或更难阅读)。但隐式类型绝不会改变 OCaml 是静态类型的事实。相反,类型检查器必须更加复杂,因为它必须推断出如果程序员编写了所有类型注释,那么类型注释“将会是什么”。原则上,类型推断和类型检查可以是单独的过程(推断器可以找出类型,然后检查器可以确定程序的类型是否正确),但实际上它们通常合并为一个称为类型重建的单个过程。

9.6.1. OCaml Type Reconstruction
9.6.1. OCaml 类型重构 ¶

At a very high level, OCaml’s type reconstruction algorithm works as follows:
在非常高的层面上,OCaml 的类型重建算法的工作原理如下:

  • Determine the types of definitions in order, using the types of earlier definitions to infer the types of later ones. (Which is one reason you may not use a name before it is bound in an OCaml program.)
    按顺序确定定义的类型,使用较早定义的类型来推断后面定义的类型。 (这是在 OCaml 程序中绑定名称之前不得使用名称的原因之一。)

  • For each let definition, analyze the definition to determine constraints about its type. For example, if the inferencer sees x + 1, it concludes that x must have type int. It gathers similar constraints for function applications, pattern matches, etc. Think of these constraints as a system of equations like you might have in algebra.
    对于每个 let 定义,分析该定义以确定有关其类型的约束。例如,如果推理器看到 x + 1 ,则得出结论 x 必须具有类型 int 。它为函数应用、模式匹配等收集类似的约束。将这些约束视为一个方程组,就像代数中的方程组一样。

  • Use that system of equations to solve for the type of the name begin defined.
    使用该方程组来求解开始定义的名称类型。

The OCaml type reconstruction algorithm attempts to never reject a program that could type check, if the programmer had written down types. It also attempts never to accept a program that cannot possibly type check. Some more obscure parts of the language can sometimes make type annotations either necessary or at least helpful (see Real World OCaml chapter 22, “Type inference”, for examples). But for most code you write, type annotations really are completely optional.
如果程序员写下了类型,OCaml 类型重构算法永远不会拒绝可以进行类型检查的程序。它还尝试永远不接受无法进行类型检查的程序。语言中一些更晦涩的部分有时会使类型注释变得必要或至少有帮助(例如,请参阅现实世界 OCaml 第 22 章“类型推断”)。但对于您编写的大多数代码来说,类型注释实际上是完全可选的。

Since it would be verbose to keep writing “the type reconstruction algorithm used by OCaml and other functional languages,” we’ll call the algorithm HM. That name is used throughout the programming languages literature, because the algorithm was independently invented by Roger Hindley and Robin Milner.
由于继续编写“OCaml 和其他函数式语言使用的类型重建算法”会很冗长,因此我们将算法称为 HM。该名称在整个编程语言文献中都使用,因为该算法是由 Roger Hindley 和 Robin Milner 独立发明的。

HM has been rediscovered many times by many people. Curry used it informally in the 1950’s (perhaps even the 1930’s). He wrote it up formally in 1967 (published 1969). Hindley discovered it independently in 1969; Morris in 1968; and Milner in 1978. In the realm of logic, similar ideas go back perhaps as far as Tarski in the 1920’s. Commenting on this history, Hindley wrote,
HM已经被很多人多次重新发现。库里在 1950 年代(甚至可能是 1930 年代)非正式地使用过它。他于 1967 年正式撰写(1969 年出版)。 Hindley于1969年独立发现了它;莫里斯,1968 年;和 Milner 于 1978 年提出。在逻辑领域,类似的想法或许可以追溯到 1920 年代的 Tarski。在评论这段历史时,欣德利写道:

There must be a moral to this story of continual re-discovery; perhaps someone along the line should have learned to read. Or someone else learn to write.
这个不断重新发现的故事必定有其寓意。也许沿途有人应该学会读书。或者别人学写作。

Although we haven’t seen the HM algorithm yet, you probably won’t be surprised to learn that it’s usually very efficient—you’ve probably never had to wait for the toplevel to print the inferred types of your programs. In practice, it runs in approximately linear time. But in theory, there are some very strange programs that can cause its running-time to blow up. (Technically, it’s exponential time.) For fun, try typing the following code in utop:
虽然我们还没有看到 HM 算法,但你可能不会惊讶地发现它通常非常高效——你可能从来不需要等待顶层打印程序的推断类型。实际上,它的运行时间大约是线性的。但从理论上讲,有一些非常奇怪的程序会导致其运行时间爆炸。 (从技术上讲,这是指数时间。)为了好玩,请尝试在 utop 中键入以下代码:

# let b = true;;
# let f0 = fun x -> x + 1;;
# let f = fun x -> if b then f0 else fun y -> x y;;
# let f = fun x -> if b then f else fun y -> x y;;
# let f = fun x -> if b then f else fun y -> x y;;
(* keep repeating that last line *)

You’ll see the types get longer and longer, and eventually (around 20 repetitions or so) type inference will cause a significant delay.
您会看到类型变得越来越长,最终(大约 20 次重复)类型推断将导致显着的延迟。

9.6.2. Constraint-Based Inference
9.6.2. 基于约束的推理 ¶

Let’s build up to the HM type inference algorithm by starting with this little language:
让我们从这个小语言开始构建 HM 类型推断算法:

e ::= x | i | b | e1 bop e2
    | if e1 then e2 else e3
    | fun x -> e
    | e1 e2

bop ::= + | * | <=

t ::= int | bool | t1 -> t2

That language is SimPL, plus the lambda calculus, minus let expressions. It turns out let expressions add a extra layer of complication, so we’ll come back to them later.
该语言是 SimPL,加上 lambda 演算,减去 let 表达式。事实证明 let 表达式增加了一层额外的复杂性,所以我们稍后再讨论它们。

Since anonymous functions in this language do not have type annotations, we have to infer the type of the argument x. For example,
由于这种语言中的匿名函数没有类型注释,因此我们必须推断参数 x 的类型。例如,

  • In fun x -> x + 1, argument x must have type int hence the function has type int -> int.
    fun x -> x + 1 中,参数 x 必须具有类型 int ,因此该函数具有类型 int -> int

  • In fun x -> if x then 1 else 0, argument x must have type bool hence the function has type bool -> int.
    fun x -> if x then 1 else 0 中,参数 x 必须具有类型 bool ,因此该函数具有类型 bool -> int

  • Function fun x -> if x then x else 0 is untypeable, because it would require x to have both type int and bool, which isn’t allowed.
    函数 fun x -> if x then x else 0 是不可输入的,因为它要求 x 具有 intbool 类型,这是不允许的。

A Syntactic Simplification. We can treat e1 bop e2 as syntactic sugar for ( bop ) e1 e2. That is, we treat infix binary operators as prefix function application. Let’s introduce a new syntactic class n for names, which generalize identifiers and operators. That changes the syntax to:
语法简化。我们可以将 e1 bop e2 视为 ( bop ) e1 e2 的语法糖。也就是说,我们将中缀二元运算符视为前缀函数应用。让我们为名称引入一个新的语法类 n ,它概括了标识符和运算符。这会将语法更改为:

e ::= n | i | b
    | if e1 then e2 else e3
    | fun x -> e
    | e1 e2

n ::= x | bop

bop ::= ( + ) | ( * ) | ( <= )

t ::= int | bool | t1 -> t2

We already know the types of those built-in operators:
我们已经知道这些内置运算符的类型:

( + ) : int -> int -> int
( * ) : int -> int -> int
( <= ) : int -> int -> bool

Those types are given; we don’t have to infer them. They are part of the initial static environment. In OCaml those operator names could later be shadowed by values with different types, but here we don’t have to worry about that because we don’t yet have let.
这些类型已给出;我们不必推断它们。它们是初始静态环境的一部分。在 OCaml 中,这些运算符名称稍后可能会被不同类型的值遮盖,但在这里我们不必担心这一点,因为我们还没有 let

How would you mentally infer the type of fun x -> 1 + x, or rather, fun x -> ( + ) 1 x? It’s automatic by now, but we could break it down into pieces:
您如何在心里推断 fun x -> 1 + x 的类型,或者更确切地说, fun x -> ( + ) 1 x 的类型?现在它是自动的,但我们可以将其分解为几个部分:

  • Start with x having some unknown type t.
    从具有某种未知类型 tx 开始。

  • Note that ( + ) is known to have type int -> (int -> int).
    请注意, ( + ) 已知具有类型 int -> (int -> int)

  • So its first argument must have type int. Which 1 does.
    因此它的第一个参数必须具有类型 int1 就是这样做的。

  • And its second argument must have type int, too. So t = int. That is a constraint on t.
    它的第二个参数也必须具有类型 int 。所以 t = int 。这是对 t 的限制。

  • Finally the body of the function must also have type int, since that’s the return type of ( + ).
    最后,函数体还必须具有类型 int ,因为这是 ( + ) 的返回类型。

  • Therefore the type of the entire function must be t -> int.
    因此整个函数的类型必须是 t -> int

  • Since t = int, that type is int -> int.
    由于 t = int ,该类型为 int -> int

The type inference algorithm follows the same idea of generating unknown types, collecting constraints on them, and using the constraints to solve for the type of the expression.
类型推断算法遵循相同的思想:生成未知类型,收集它们的约束,并使用约束来求解表达式的类型。

Let’s introduce a new quaternary relation env |- e : t -| C, which should be read as follows: “in environment env, expression e is inferred to have type t and generates constraint set C.” A constraint is an equation of the form t1 = t2 for any types t1 and t2.
让我们引入一个新的四元关系 env |- e : t -| C ,它应该读作如下:“在环境 env 中,表达式 e 被推断为具有类型 t 。”约束是任何类型 t1t2t1 = t2 形式的方程。

If we think of the relation as a type-inference function, the colon in the middle separates the input from the output. The inputs are env and e: we want to know what the type of e is in environment env. The function returns as output a type t and constraints C.
如果我们将关系视为类型推断函数,则中间的冒号将输入与输出分开。输入是 enve :我们想知道环境 enve 的类型是什么。该函数返回类型 t 和约束 C 作为输出。

The e : t in the middle of the relation is approximately what you see in the toplevel: you enter an expression, and it tells you the type. But around that is an environment and constraint set env |- ... -| C that is invisible to you. So, the turnstiles around the outside show the parts of type inference that the toplevel does not.
关系中间的 e : t 大约是您在顶层看到的内容:您输入一个表达式,它会告诉您类型。但围绕它的是一个你不可见的环境和约束集 env |- ... -| C 。因此,外部的十字转门显示了顶层没有的类型推断部分。

The easiest parts of inference are constants:
推理最简单的部分是常数:

env |- i : int -| {}

env |- b : bool -| {}

Any integer constant i, such as 42, is known to have type int, and there are no constraints generated. Likewise for Boolean constants.
已知任何整数常量 i ,例如 42 ,都具有类型 int ,并且不会生成任何约束。对于布尔常量也是如此。

Inferring the type of a name requires looking it up in the environment:
推断名称的类型需要在环境中查找:

env |- n : env(n) -| {}

No constraints are generated.
不产生约束。

If the name is not bound in the environment, the expression cannot be typed. It’s an unbound name error.
如果名称未绑定在环境中,则无法键入表达式。这是一个未绑定名称错误。

The remaining rules are at their core the same as the type-checking rules we saw previously, but they each generate a type variable and possibly some constraints on that type variable.
其余规则的核心与我们之前看到的类型检查规则相同,但它们各自生成一个类型变量以及可能对该类型变量的一些约束。

If.

Here’s the rule for if expressions:
这是 if 表达式的规则:

env |- if e1 then e2 else e3 : 't -| C1, C2, C3, t1 = bool, 't = t2, 't = t3
  if fresh 't
  and env |- e1 : t1 -| C1
  and env |- e2 : t2 -| C2
  and env |- e3 : t3 -| C3

To infer the type of an if, we infer the types t1, t2, and t3 of each of its subexpressions, along with any constraints on them. We have no control over what those types might be; it depends on what the programmer wrote. But we do know that the type of the guard must be bool. So we generate a constraint that t1 = bool.
为了推断 if 的类型,我们推断其每个子表达式的 t1t2t3 类型,以及对他们的任何限制。我们无法控制这些类型可能是什么;这取决于程序员写的内容。但我们确实知道守卫的类型必须是 bool 。所以我们生成一个约束 t1 = bool

Furthermore, we know that both branches must have the same type—though, we don’t know in advance what that type might be. So, we invent a fresh type variable 't to stand for that type. A type variable is fresh if it has never been used elsewhere during type inference. So, picking a fresh type variable just means picking a new name that can’t possibly be confused with any other names in the program. We return 't as the type of the if, and we record two constraints 't = t2 and 't = t3 to say that both branches must have that type.
此外,我们知道两个分支必须具有相同的类型——尽管我们事先并不知道该类型可能是什么。因此,我们发明了一个新的类型变量 't 来代表该类型。如果类型变量在类型推断期间从未在其他地方使用过,则该类型变量是新鲜的。因此,选择一个新的类型变量只是意味着选择一个新名称,该名称不可能与程序中的任何其他名称混淆。我们返回 't 作为 if 的类型,并记录两个约束 't = t2't = t3 来表示两个分支都必须具有该约束类型。

We therefore need to add type variables to the syntax of types:
因此,我们需要将类型变量添加到类型语法中:

t ::= 'x | int | bool | t1 -> t2

Some example type variables include 'a, 'foobar, and 't. In the last, t is an identifier, not a meta-variable.
一些示例类型变量包括 'a'foobar't 。最后, t 是一个标识符,而不是元变量。

Here’s an example: 这是一个例子:

{} |- if true then 1 else 0 : 't -| bool = bool, 't = int
  {} |- true : bool -| {}
  {} |- 1 : int -| {}
  {} |- 0 : int -| {}

The full constraint set generated is {}, {}, {}, bool = bool, 't = int, 't = int, but of course that simplifies to just bool = bool, 't = int. From that constraint set we can see that the type of if true then 1 else 0 must be int.
生成的完整约束集是 {}, {}, {}, bool = bool, 't = int, 't = int ,但当然可以简化为 bool = bool, 't = int 。从该约束集中我们可以看到 if true then 1 else 0 的类型必须是 int

Anonymous functions. 匿名函数。

Since there is no type annotation on x, its type must be inferred:
由于 x 上没有类型注释,因此必须推断其类型:

env |- fun x -> e : 't1 -> t2 -| C
  if fresh 't1
  and env, x : 't1 |- e : t2 -| C

We introduce a fresh type variable 't1 to stand for the type of x, and infer the type of body e under the environment in which x : 't1. Wherever x is used in e, that can cause constraints to be generated involving 't1. Those constraints will become part of C.
我们引入一个新的类型变量 't1 来代表 x 的类型,并在 x : 't1e 中使用,都可能导致生成涉及 't1 的约束。这些约束将成为 C 的一部分。

Here’s a function where we can immediately see that x : bool, but let’s work through the inference:
在下面的函数中,我们可以立即看到 x : bool ,但让我们进行推理:

{} |- fun x -> if x then 1 else 0 : 't1 -> 't -| 't1 = bool, 't = int
  {}, x : 't1 |- if x then 1 else 0 : 't -| 't1 = bool, 't = int
    {}, x : 't1 |- x : 't1 -| {}
    {}, x : 't1 |- 1 : int -| {}
    {}, x : 't1 |- 0 : int -| {}

The inferred type of the function is 't1 -> 't, with constraints 't1 = bool and 't = int. Simplifying that, the function’s type is bool -> int.
函数的推断类型为 't1 -> 't ,具有约束 't1 = bool't = int 。简化一下,函数的类型是 bool -> int

Function application. 函数应用。

The type of the entire application must be inferred, because we don’t yet know anything about the types of either subexpression:
必须推断整个应用程序的类型,因为我们还不知道任何一个子表达式的类型:

env |- e1 e2 : 't -| C1, C2, t1 = t2 -> 't
  if fresh 't
  and env |- e1 : t1 -| C1
  and env |- e2 : t2 -| C2

We introduce a fresh type variable 't for the type of the application expression. We use inference to determine the types of the subexpressions and any constraints they happen to generate. We add one new constraint, t1 = t2 -> 't, which expresses that the type of the left-hand side e1 must be a function that takes in an argument of type t2 and returns a value of type 't.
我们为应用程序表达式的类型引入了一个新的类型变量 't 。我们使用推理来确定子表达式的类型以及它们碰巧生成的任何约束。我们添加一个新约束 t1 = t2 -> 't ,它表示左侧 e1 的类型必须是一个接受 t2 类型参数的函数并返回 't 类型的值。

Let I be the initial environment that binds the boolean operators. Let’s infer the type of a partial application of ( + ):
I 为绑定布尔运算符的初始环境。让我们推断一下 ( + ) 的部分应用的类型:

I |- ( + ) 1 : 't -| int -> int -> int = int -> 't
  I |- ( + ) : int -> int -> int -| {}
  I |- 1 : int -| {}

From the resulting constraint, we see that
从由此产生的约束中,我们看到

int -> int -> int
=
int -> 't

Stripping the int -> off the left-hand side of each of those function types, we are left with
去掉每个函数类型左侧的 int -> ,我们剩下

int -> int
=
't

Hence the type of ( + ) 1 is int -> int.
因此 ( + ) 1 的类型是 int -> int

9.6.3. Solving Constraints
9.6.3. 解决约束 ¶

What does it mean to solve a set of constraints? Since constraints are equations on types, it’s much like solving a system of equations in algebra. We want to solve for the values of the variables appearing in those equations. By substituting those values for the variables, we should get equations that are identical on both sides. For example, in algebra we might have:
解决一组约束意味着什么?由于约束是类型上的方程,因此它很像求解代数方程组。我们想要求解这些方程中出现的变量的值。通过用这些值替换变量,我们应该得到两边相同的方程。例如,在代数中我们可能有:

5x + 2y =  9
 x -  y = -1

Solving that system, we’d get that x = 1 and y = 2. If we substitute 1 for x and 2 for y, we get:
求解该系统,我们会得到 x = 1y = 2 。如果我们用 1 代替 x ,用 2 代替 y ,我们得到:

5(1) + 2(2) =  9
  1  -   2  = -1

which reduces to 这减少到

 9 =  9
-1 = -1

In programming languages terminology (though perhaps not high-school algebra), we say that the substitutions {1 / x} and {2 / y} together unify that set of equations, because they make each equation “unite” such that its left side is identical to its right side.
在编程语言术语中(尽管可能不是高中代数),我们说替换 {1 / x}{2 / y} 一起统一了该组方程,因为它们使每个方程“统一”为它的左侧与右侧相同。

Solving systems of equations on types is similar. Just as we found numbers to substitute for variables above, we now want to find types to substitute for type variables, and thereby unify the set of equations.
求解类型方程组是类似的。正如我们找到数字来代替上面的变量一样,我们现在想要找到类型来代替类型变量,从而统一方程组。

Much like the substitutions we defined before for the substitution model of evaluation, we’ll write {t / 'x} for the type substitution that maps type variable 'x to type t. For example, {t2/'x} t1 means type t1 with t2 substituted for 'x.
与我们之前为求值替换模型定义的替换非常相似,我们将编写 {t / 'x} 来表示将类型变量 'x 映射到类型 t 的类型替换。例如, {t2/'x} t1 表示类型 t1 ,并用 t2 替换 'x

We can define substitution on types as follows:
我们可以如下定义类型的替换:

int {t / 'x} = int
bool {t / 'x} = bool
'x {t / 'x} = t
'y {t / 'x} = 'y
(t1 -> t2) {t / 'x} =  (t1 {t / 'x} ) -> (t2 {t / 'x} )

Given two substitutions S1 and S2, we write S1; S2 to mean the substitution that is their sequential composition, which is defined as follows:
给定两个替换 S1S2 ,我们写 S1; S2 来表示替换是它们的顺序组合,其定义如下:

t (S1; S2) = (t S1) S2

The order matters. For example, 'x ({('y -> 'y) / 'x}; {bool / 'y}) is bool -> bool, not 'y -> 'y. We can build up bigger and bigger substitutions this way.
顺序很重要。例如, 'x ({('y -> 'y) / 'x}; {bool / 'y}) bool -> bool ,而不是 'y -> 'y 。我们可以通过这种方式建立越来越大的替代品。

A substitution S can be applied to a constraint t = t'. The result (t = t') S is defined to be t S = t' S. So we just apply the substitution on both sides of the constraint.
替换 S 可以应用于约束 t = t' 。结果 (t = t') S 定义为 t S = t' S 。所以我们只需在约束的两侧应用替换即可。

Finally a substitution can be applied to a set C of constraints; the result C S is the result of applying S to each of the individual constraints in C.
最后,可以将替换应用于一组约束 C ;结果 C S 是将 S 应用于 C 中每个单独约束的结果。

A substitution unifies a constraint t_1 = t_2 if t_1 S results in the same type as t_2 S. For example, substitution S = {int -> int / 'y}; {int / 'x} unifies constraint 'x -> ('x -> int) = int -> 'y, because
如果 t_1 S 产生与 t_2 S 相同的类型,则替换会统一约束 t_1 = t_2 。例如,替换 S = {int -> int / 'y}; {int / 'x} 统一了约束 'x -> ('x -> int) = int -> 'y ,因为

('x -> ('x -> int)) S
=
int -> (int -> int)

and

(int -> 'y) S
=
int -> (int -> int)

A substitution S unifies a set C of constraints if S unifies every constraint in C.
如果 S 统一了 C 中的每个约束,则替换 S 会统一一组 C 约束。

At last we can precisely say what it means to solve a set of constraints: we must find a substitution that unifies the set. That is, we need to find a sequence of maps from type variables to types, such that the sequence causes each equation in the constraint set to “unite”, meaning that its left-hand side and right-hand side become the same.
最后我们可以准确地说解决一组约束意味着什么:我们必须找到统一该组的替代。也就是说,我们需要找到从类型变量到类型的映射序列,使得该序列使约束集中的每个方程“联合”,这意味着其左侧和右侧变得相同。

To find a substitution that unifies constraint set C, we use an algorithm unify, which is defined as follows:
为了找到统一约束集 C 的替换,我们使用算法 unify ,其定义如下:

  • If C is the empty set, then unify(C) is the empty substitution.
    如果 C 是空集,则 unify(C) 是空替换。

  • If C contains at least one constraint t1 = t2 and possibly some other constraints C', then unify(C) is defined as follows:
    如果 C 包含至少一个约束 t1 = t2 以及可能的一些其他约束 C' ,则 unify(C) 定义如下:

    • If t1 and t2 are both the same simple type—i.e. both the same type variable 'x, or both int or both bool— then return unify(C'). In this case, the constraint contained no useful information, so we’re tossing it out and continuing.
      如果 t1t2 都是相同的简单类型,即都是相同类型的变量 'x ,或者都是 int ,或者都是 bool ——然后返回 unify(C') 。在本例中,约束不包含任何有用的信息,因此我们将其丢弃并继续。

    • If t1 is a type variable 'x and 'x does not occur in t2, then let S = {t2 / 'x}, and return S; unify(C' S). In this case, we are eliminating the variable 'x from the system of equations, much like Gaussian elimination in solving algebraic equations.
      如果 t1 是类型变量 'x 并且 'x 未出现在 t2 中,则让 S = {t2 / 'x} ,并且返回 S; unify(C' S) 。在这种情况下,我们从方程组中消除变量 'x ,就像求解代数方程中的高斯消元法一样。

    • If t2 is a type variable 'x and 'x does not occur in t1, then let S = {t1 / 'x}, and return S; unify(C' S). This is an elimination like the previous case.
      如果 t2 是类型变量 'x 并且 'x 未出现在 t1 中,则让 S = {t1 / 'x} ,并且返回 S; unify(C' S) 。这是与前一个案例类似的消除。

    • If t1 = i1 -> o1 and t2 = i2 -> o2, where i1, i2, o1, and o2 are types, then unify(i1 = i2, o1 = o2, C'). In this case, we break one constraint down into two smaller constraints and add those constraints back in to be further unified.
      如果 t1 = i1 -> o1t2 = i2 -> o2 ,其中 i1i2o1o2 是类型,然后是 unify(i1 = i2, o1 = o2, C') 。在这种情况下,我们将一个约束分解为两个较小的约束,并将这些约束添加回来以进一步统一。

    • Otherwise, fail. There is no possible unifier.
      否则,失败。没有可能的统一者。

In the second and third sub-cases, the check that 'x should not occur in the type ensures that the algorithm is actually eliminating the variable. Otherwise, the algorithm could end up re-introducing the variable instead of eliminating it.
在第二个和第三个子情况中,检查 'x 不应出现在类型中可确保算法实际上消除了变量。否则,算法最终可能会重新引入变量而不是消除它。

It’s possible to prove that the unification algorithm always terminates, and that it produces a result if and only if a unifier actually exists—that is, if and only if the set of constraints has a solution. Moreover, the solution the algorithm produces is the most general unifier, in the sense that if S = unify(C) and S' also unifies C, then there must exist some S'' such that S' = S; S''. Such an S' is less general than S because it contains the additional substitutions of S''.
可以证明统一算法总是终止,并且当且仅当统一器实际存在时它才会产生结果,即当且仅当约束集有解时。此外,该算法产生的解决方案是最通用的统一符,如果 S = unify(C)S' 也统一 C ,那么一定存在一些 S'' 使得 S' = S; S'' 。这样的 S' 不如 S 通用,因为它包含 S'' 的额外替换。

9.6.4. Finishing Type Inference
9.6.4. 完成理类型推断 ¶

Let’s review what we’ve done so far. We started with this language:
让我们回顾一下到目前为止我们所做的事情。我们从这种语言开始:

e ::= n | i | b
    | if e1 then e2 else e3
    | fun x -> e
    | e1 e2

n ::= x | bop

bop ::= ( + ) | ( * ) | ( <= )

t ::= int | bool | t1 -> t2

We then introduced an algorithm for inferring a type of an expression. That type came along with a set of constraints. The algorithm was expressed in the form of a relation env |- e : t -| C.
然后我们引入了一种用于推断表达式类型的算法。这种类型伴随着一系列限制。该算法以关系 env |- e : t -| C 的形式表达。

Next, we introduced the unification algorithm for solving constraint sets. That algorithm produces as output a sequence S of substitutions, or it fails. If it fails, then e is not typeable.
接下来,我们介绍了求解约束集的统一算法。该算法生成替换序列 S 作为输出,否则就会失败。如果失败,则 e 不可输入。

To finish type inference and reconstruct the type of e, we just compute t S. That is, we apply the solution to the constraints to the type t produced by constraint generation.
为了完成类型推断并重建 e 的类型,我们只需计算 t S 。也就是说,我们将解决方案应用于由约束生成生成的类型 t 的约束。

Let p be that type. That is, p = t S. It’s possible to prove p is the principal type for the expression, meaning that if e also has type t for any other t, then there exists a substitution S such that t = p S.
p 为该类型。即 p = t S 。可以证明 p 是表达式的主要类型,这意味着如果 e 对于任何其他 t 也具有类型 t ,那么存在一个替换 S 使得 t = p S

For example, the principal type of the identity function fun x -> x would be 'a -> 'a. But you could also give that function the less helpful type int -> int. What we’re saying is that HM will produce 'a -> 'a, not int -> int. So in a sense, HM actually infers the most “lenient” type that is possible for an expression.
例如,恒等函数 fun x -> x 的主体类型为 'a -> 'a 。但您也可以为该函数指定不太有用的类型 int -> int 。我们的意思是 HM 将产生 'a -> 'a ,而不是 int -> int 。因此,从某种意义上说,HM 实际上推断了表达式可能的最“宽松”类型。

A Worked Example. Let’s infer the type of the following expression:
一个可用的例子。我们来推断以下表达式的类型:

fun f -> fun x -> f (( + ) x 1)

It’s not much code, but this will get quite involved!
代码不多,但这会很复杂!

We start in the initial environment I that, among other things, maps ( + ) to int -> int -> int.
我们从初始环境 I 开始,其中将 ( + ) 映射到 int -> int -> int

I |- fun f -> fun x -> f (( + ) x 1)

For now we leave off the : t -| C, because that’s the output of constraint generation. We haven’t figure out the output yet! Since we have a function, we use the function rule for inference to proceed by introducing a fresh type variable for the argument:
现在我们保留 : t -| C ,因为这是约束生成的输出。我们还没有弄清楚输出!由于我们有一个函数,因此我们使用函数规则进行推理,为参数引入一个新的类型变量:

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)  <-- Here

Again we have a function, hence a fresh type variable:
我们再次有了一个函数,因此有了一个新的类型变量:

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)  <-- Here

Now we have an application expression. Before dealing with it, we need to descend into its subexpressions. The first one is easy. It’s just a variable. So we finally can finish a judgment with the variable’s type from the environment, and an empty constraint set.
现在我们有了一个应用程序表达式。在处理它之前,我们需要深入了解它的子表达式。第一个很容易。这只是一个变量。这样我们最终就可以完成对环境变量类型和空约束集的判断。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}  <-- Here

Next is the second subexpression.
接下来是第二个子表达式。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1  <-- Here

That is another application, so we need to handle its subexpressions. Recall that ( + ) x 1 is parsed as (( + ) x) 1. So the first subexpression is the complicated one to handle.
这是另一个应用程序,因此我们需要处理它的子表达式。回想一下 ( + ) x 1 被解析为 (( + ) x) 1 。所以第一个子表达式是处理起来比较复杂的。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1
        I, f : 'a, x : 'b |- ( + ) x  <-- Here

Yet another application.
又一个应用程序。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1
        I, f : 'a, x : 'b |- ( + ) x
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}  <-- Here

That one was easy, because we just had to look up the name ( + ) in the environment. The next is also easy, because we just look up x.
这很简单,因为我们只需在环境中查找名称 ( + ) 即可。接下来也很简单,因为我们只需查找 x

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1
        I, f : 'a, x : 'b |- ( + ) x
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}  <-- Here

At last, we’re ready to resolve a function application! We introduce a fresh type variable and add a constraint. The constraint is that the inferred type int -> int -> int of the left-hand subexpression must equal the inferred type 'b of the right-hand subexpression arrow the fresh type variable 'c, that is, 'b -> 'c.
最后,我们准备好解析函数应用程序了!我们引入一个新的类型变量并添加一个约束。约束是左侧子表达式的推断类型 int -> int -> int 必须等于右侧子表达式箭头的推断类型 'b 新鲜类型变量 'c ,即 'b -> 'c

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1
        I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c  <-- Here
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}

Now we’re ready for the argument being passed to that function.
现在我们已经准备好将参数传递给该函数了。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1
        I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}
        I, f : 'a, x : 'b |- 1 : int -| {}  <-- Here

Again we can resolve a function application with a new type variable and constraint.
我们再次可以使用新的类型变量和约束来解析函数应用程序。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1)
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1 : 'd -| 'c = int -> 'd, int -> int -> int = 'b -> 'c  <-- Here
        I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}
        I, f : 'a, x : 'b |- 1 : int -| {}

And once more, a function application, so a new type variable and a new constraint.
再一次,函数应用,新的类型变量和新的约束。

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1)
    I, f : 'a, x : 'b |- f (( + ) x 1) : 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c   <-- Here
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1 : 'd -| 'c = int -> 'd, int -> int -> int = 'b -> 'c
        I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}
        I, f : 'a, x : 'b |- 1 : int -| {}

Now we finally get to finish off an anonymous function. Its inferred type is the fresh type variable 'b of its parameter x, arrow the inferred type e of its body.
现在我们终于完成了一个匿名函数。它的推断类型是其参数 x 的新类型变量 'b ,箭头是其主体的推断类型 e

I |- fun f -> fun x -> f (( + ) x 1)
  I, f : 'a |- fun x -> f (( + ) x 1) : 'b -> 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c   <-- Here
    I, f : 'a, x : 'b |- f (( + ) x 1) : 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c
      I, f : 'a, x : 'b |- f : 'a -| {}
      I, f : 'a, x : 'b |- ( + ) x 1 : 'd -| 'c = int -> 'd, int -> int -> int = 'b -> 'c
        I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c
          I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
          I, f : 'a, x : 'b |- x : 'b -| {}
        I, f : 'a, x : 'b |- 1 : int -| {}

And the last anonymous function can now be complete in the same way:
现在可以用同样的方式完成最后一个匿名函数:

I |- fun f -> fun x -> f (( + ) x 1) : 'a -> 'b -> 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c  <-- Here
  I, f : 'a |- fun x -> f (( + ) x 1) : 'b -> 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c
    I, f : 'a, x : 'b |- f (( + ) x 1) : 'e -| 'a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c
       I, f : 'a, x : 'b |- f : 'a -| {}
       I, f : 'a, x : 'b |- ( + ) x 1 : 'd -| 'c = int -> 'd, int -> int -> int = 'b -> 'c
         I, f : 'a, x : 'b |- ( + ) x : 'c -| int -> int -> int = 'b -> 'c
           I, f : 'a, x : 'b |- ( + ) : int -> int -> int -| {}
           I, f : 'a, x : 'b |- x : 'b -| {}
         I, f : 'a, x : 'b |- 1 : int -| {}

As a result of constraint generation, we know that the type of the expression is 'a -> 'b -> 'e, where
作为约束生成的结果,我们知道表达式的类型是 'a -> 'b -> 'e ,其中

'a = 'd -> 'e
'c = int -> 'd
int -> int -> int = 'b -> 'c

To solve that system of equations, we use the unification algorithm:
为了求解该方程组,我们使用统一算法:

unify('a = 'd -> 'e, 'c = int -> 'd, int -> int -> int = 'b -> 'c)

The first constraint yields a substitution {('d -> 'e) / 'a}, which we record as part of the solution, and also apply it to the remaining constraints:
第一个约束产生一个替换 {('d -> 'e) / 'a} ,我们将其记录为解决方案的一部分,并将其应用于其余约束:

...
=
{('d -> 'e) / 'a}; unify(('c = int -> 'd, int -> int -> int = 'b -> 'c) {('d -> 'e) / 'a}
=
{('d -> 'e) / 'a}; unify('c = int -> 'd, int -> int -> int = 'b -> 'c)

The second constraint behaves similarly to the first:
第二个约束的行为与第一个约束类似:

...
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; unify((int -> int -> int = 'b -> 'c) {(int -> 'd) / 'c})
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; unify(int -> int -> int = 'b -> int -> 'd)

The function constraint breaks down into two smaller constraints:
函数约束分解为两个较小的约束:

...
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; unify(int = 'b, int -> int = int -> 'd)

We get another substitution:
我们得到另一个替换:

...
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; unify((int -> int = int -> 'd) {int / 'b})
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; unify(int -> int = int -> 'd)

Then we get to break down another function constraint:
然后我们打破另一个函数约束:

...
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; unify(int = int, int = 'd)

The first of the resulting new constraints is trivial and just gets dropped:
由此产生的第一个新约束是微不足道的,只是被删除:

...
=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; unify(int = 'd)

The very last constraint gives us one more substitution:
最后一个约束给了我们另一个替代:

=
{('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; {int / 'd}

To finish, we apply the substitution output by unification to the type inferred by constraint generation:
最后,我们将统一的替换输出应用于约束生成推断的类型:

('a -> 'b -> 'e) {('d -> 'e) / 'a}; {(int -> 'd) / 'c}; {int / 'b}; {int / 'd}
=
(('d -> 'e) -> 'b -> 'e) {(int -> 'd) / 'c}; {int / 'b}; {int / 'd}
=
(('d -> 'e) -> 'b -> 'e) {int / 'b}; {int / 'd}
=
(('d -> 'e) -> int -> 'e) {int / 'd}
=
(int -> 'e) -> int -> 'e

And indeed that is the same type that OCaml would infer for the original expression:
事实上,这与 OCaml 为原始表达式推断出的类型相同:

# fun f -> fun x -> f (( + ) x 1);;
- : (int -> 'a) -> int -> 'a = <fun>

Except that OCaml uses a different type variable identifier. OCaml is nice to us and “lowers” the type variables down to smaller letters of the alphabet. We could do that too with a little extra work.
除了 OCaml 使用不同的类型变量标识符之外。 OCaml 对我们很好,并将类型变量“降低”为字母表中较小的字母。我们也可以通过一些额外的工作来做到这一点。

Type Errors. In reality there is yet another piece to type inference. If unification fails, the compiler or interpreter needs to produce a helpful error message. That’s an important engineering challenge that we won’t address here. It requires keeping track of more than just constraints: we need to know why a constraint was introduced, and the ramification of its violation. We also need to track the constraint back to the lexical piece of code that produced it, so that programmers can see where the problem occurs. And since it’s possible that constraints can be processed in many different orders, there are many possible error messages that could be produced. Figuring out which one will lead the programmer to the root cause of an error, instead of some downstream consequence of it, is an area of ongoing research.
类型错误。事实上,还有另一部分需要类型推理。如果统一失败,编译器或解释器需要生成有用的错误消息。这是一个重要的工程挑战,我们在此不予讨论。它需要跟踪的不仅仅是约束:我们需要知道为什么引入约束,以及违反约束的后果。我们还需要将约束追溯到产生它的词法代码段,以便程序员可以看到问题发生在哪里。由于约束可能以多种不同的顺序进行处理,因此可能会产生多种错误消息。找出哪一个将导致程序员找到错误的根本原因,而不是错误的下游后果,是一个正在进行的研究领域。

9.6.5. Let Polymorphism
9.6.5. Let 多态 ¶

Now we’ll add let expressions to our little language:
现在我们将向我们的小语言添加 let 表达式:

e ::= x | i | b | e1 bop e2
    | if e1 then e2 else e3
    | fun x -> e
    | e1 e2
    | let x = e1 in e2   (* new *)

It turns out type inference for them is considerably trickier than might be expected. The naive approach would be to add this constraint generation rule:
事实证明,它们的类型推断比预期的要棘手得多。最简单的方法是添加此约束生成规则:

env |- let x = e1 in e2 : t2 -| C1, C2
  if env |- e1 : t1 -| C1
  and env, x : t1 |- e2 : t2 -| C2

From the type-checking perspective, that’s the same rule we’ve always used. And for many let expressions it works perfectly fine. For example:
从类型检查的角度来看,这与我们一直使用的规则相同。对于许多 let 表达式来说,它工作得很好。例如:

{} |- let x = 42 in x : int -| {}
  {} |- 42 : int -| {}
  x : int |- x : int -| {}

The problem is that when the value being bound is a polymorphic function, that rule generates constraints that are too restrictive. For example, consider the identity function:
问题在于,当绑定的值是多态函数时,该规则会生成限制性太强的约束。例如,考虑恒等函数:

let id = fun x -> x in
let a = id 0 in
id true

OCaml has no trouble inferring the type of id as 'a -> 'a and permitting it to be applied both to an int and a bool. But the rule above isn’t so permissive about application to both types. When we use it, we generate the following types and constraints:
OCaml 可以轻松地将 id 的类型推断为 'a -> 'a 并允许将其应用于 intbool 。但上述规则对于这两种类型的应用并不那么宽松。当我们使用它时,我们生成以下类型和约束:

{} |- let id = fun x -> x in (let a = id 0 in id true) : 'c -| 'a -> 'a = int -> 'b, 'a -> 'a = bool -> 'c
  {} |- fun x -> x : 'a -| {}
    x : 'a |- x : 'a -| {}
  id : 'a -> 'a |- let a = id 0 in id true : 'c -| 'a -> 'a = int -> 'b, 'a -> 'a = bool -> 'c   <--- POINT 1
    id : 'a -> 'a |- id 0 : 'b -| 'a -> 'a = int -> 'b
      id : 'a -> 'a |- id : 'a -> 'a -| {}
      id : 'a -> 'a |- 0 : int -| {}
    id : 'a -> 'a, a : 'b |- id true : 'c -| 'a -> 'a = bool -> 'c   <--- POINT 2
      id : 'a -> 'a, a : 'b |- id : 'a -> 'a -| {}
      id : 'a -> 'a, a : 'b |- true : bool -| {}

Notice that we do infer a type 'a -> 'a for id, which you can see in the environment in later lines of the example. But, at Point 1, we infer a constraint 'a -> 'a = int -> 'b, and at Point 2, we infer 'a -> 'a = bool -> 'c. When the unification algorithm encounters those constraints, it will break them down into 'a = int, ‘a = 'b, 'a = bool, and 'a = 'c. The first and third of those are contradictory, because we can’t have 'a = int and 'a = bool. One or the other will be substituted away during unification, leaving an unsatisfiable constraint int = bool. At that point unification will fail, declaring the program to be ill typed.
请注意,我们确实为 id 推断出类型 'a -> 'a ,您可以在示例后面几行的环境中看到它。但是,在第 1 点,我们推断出约束 'a -> 'a = int -> 'b ,在第 2 点,我们推断出 'a -> 'a = bool -> 'c 。当统一算法遇到这些约束时,它会将它们分解为 'a = inta = 'b'a = bool'a = 'c 。第一个和第三个是矛盾的,因为我们不能有 'a = int'a = bool 。其中之一将在统一期间被替换,留下不可满足的约束 int = bool 。到那时,统一将会失败,并宣布该程序类型错误。

The problem is that the 'a type variable in the inferred type of id stands for an unknown but fixed type. At each application of id, we want to let 'a become a different type, instead of forcing it to always be the same type.
问题在于 id 的推断类型中的 'a 类型变量代表未知但固定的类型。在每次应用 id 时,我们希望让 'a 成为不同的类型,而不是强制它始终为相同的类型。

The solution to the problem of polymorphism for let expressions is not simple. It requires us to introduce a new kind of type: a type scheme. Type schemes resemble universal quantification from mathematical logic. For example, in logic you might write, “for all natural numbers x, it holds that 0x=0”. The “for all” is the universal quantification: it abstracts away from a particular x and states a property that is true of all natural numbers.
let 表达式多态性问题的解决并不简单。它要求我们引入一种新的类型:类型方案。类型方案类似于数学逻辑中的通用量化。例如,在逻辑中您可能会这样写:“对于所有自然数 x ,它都保持 0x=0 ”。 “对于所有人”是通用量化:它从特定的 x 中抽象出来,并陈述一个适用于所有自然数的属性。

A type scheme is written 'a . t, where 'a is a type variable and t is a type in which 'a may appear. For example, 'a . 'a -> 'a is a type scheme. It is the type of a function that takes in a value of type 'a and returns a value of type 'a, for all 'a. Thus, it is the type of the polymorphic identity function.
类型方案写作 'a . t ,其中 'a 是类型变量, t 是其中可能出现 'a 的类型。例如, 'a . 'a -> 'a 是一种类型方案。对于所有 'a 来说,它是接受 'a 类型的值并返回 'a 类型的值的函数类型。因此,它是多态恒等函数的类型。

We can also have many type variables to the left of the dot in a type scheme. For example, 'a 'b . 'a -> 'b -> 'a is the type of a function that takes in two arguments and returns the first. In OCaml, we could write that as fun x y -> x. Note that utop infers the type of it as we would expect:
在类型方案中,我们还可以在点的左侧放置许多类型变量。例如, 'a 'b . 'a -> 'b -> 'a 是接受两个参数并返回第一个参数的函数类型。在 OCaml 中,我们可以将其写为 fun x y -> x 。请注意,utop 正如我们所期望的那样推断出它的类型:

# let f = fun x y -> x;;
val f : 'a -> 'b -> 'a = <fun>

But we could actually manually write down an annotation with a type scheme:
但我们实际上可以用类型方案手动写下注释:

# let f : 'a 'b . 'a -> 'b -> 'a = fun x y -> x;;
val f : 'a -> 'b -> 'a = <fun>

Note that OCaml accepts our manual type annotation but doesn’t include the 'a 'b . part of it in its output. But it’s implicitly there and always has been. In general, anytime OCaml has inferred a type t and that type has had type variables in it, in reality it’s a type scheme. For example, the type of List.length is really a type scheme:
请注意,OCaml 接受我们的手动类型注释,但在其输出中不包含其 'a 'b . 部分。但它隐含地存在并且一直存在。一般来说,只要 OCaml 推断出类型 t 并且该类型中包含类型变量,实际上它就是一个类型方案。例如, List.length 的类型实际上是一个类型方案:

# let mylen : 'a . 'a list -> int = List.length;;
val mylen : 'a list -> int = <fun>

OCaml just doesn’t bother outputting the list of type variables that are to the left of the dot in the type scheme. Really they’d just clutter the output, and many programmers never need to know about them. But now that you’re learning type inference, it’s time for you to know.
OCaml 只是不费心输出类型方案中点左侧的类型变量列表。事实上,它们只会使输出变得混乱,许多程序员永远不需要了解它们。但现在您正在学习类型推断,是时候了解一下了。

Now that we have type schemes, we’ll have static environments that map names to type schemes. We can think of types as being special cases of type schemes in which the list of type variables is empty. With type schemes, the let rule changes in only one way from the naive rule above, which is the generalize on the last line:
现在我们有了类型方案,我们将拥有将名称映射到类型方案的静态环境。我们可以将类型视为类型方案的特殊情况,其中类型变量列表为空。对于类型方案, let 规则仅以一种方式与上面的朴素规则发生变化,即最后一行的 generalize

env |- let x = e1 in e2 : t2 -| C1, C2
  if env |- e1 : t1 -| C1
  and generalize(C1, env, x : t1) |- e2 : t2 -| C2

The job of generalize is to take a type like 'a -> 'a and generalize it into a type scheme like 'a . 'a -> 'a in an environment env against constraints C1. Let’s come back to how it works in a minute. Before that, there’s one other rule that needs to change, which is the name rule:
generalize 的工作是在环境 env 中采用类似 'a -> 'a 的类型并将其泛化为类似 'a . 'a -> 'a 的类型方案(针对约束) C1 。让我们稍后回顾一下它是如何工作的。在此之前,还有另一条规则需要更改,即名称规则:

env |- n : instantiate(env(n)) -| {}

The only thing that changes there is that use of instantiate. Its job is to take a type scheme like 'a . 'a -> 'a and instantiate it into a new type (and here we strictly mean a type, not a type scheme) with fresh type variables. For example, 'a . 'a -> 'a could be instantiated as 'b -> 'b, if ‘b isn’t yet in use anywhere else as a type variable.
唯一改变的是 instantiate 的使用。它的工作是采用像 'a . 'a -> 'a 这样的类型方案,并使用新的类型变量将其实例化为新类型(这里我们严格指的是类型,而不是类型方案)。例如,如果“ b 尚未在其他任何地方用作类型变量,则 'a . 'a -> 'a 可以实例化为 'b -> 'b

Here’s how those two revised rules work together to get our earlier example with the identify function right:
以下是这两个修订后的规则如何协同工作以使我们之前的示例具有正确的识别功能:

{} |- let id = fun x -> x in (let a = id 0 in id true)
  {} |- fun x -> x : 'a -> 'a -| {}
    x : 'a |- x : 'a -| {}
  id : 'a . 'a -> 'a |- let a = id 0 in id true   <--- POINT 1

Let’s pause there at Point 1. When id is put into the environment by the let rule, its type is generalized from 'a -> 'a to 'a . 'a -> 'a; that is, from a type to a type scheme. That records the fact that each application of id should get to use its own value for 'a. Going on:
让我们在第 1 点暂停一下。当 id 通过 let 规则放入环境中时,其类型从 'a -> 'a 泛化为 'a . 'a -> 'a ;也就是说,从类型到类型方案。这记录了这样一个事实: id 的每个应用程序都应该使用自己的 'a 值。正在进行:

{} |- let id = fun x -> x in (let a = id 0 in id true)
  {} |- fun x -> x : 'a -> 'a -| {}
    x : 'a |- x : 'a -| {}
  id : 'a . 'a -> 'a |- let a = id 0 in id true   <--- POINT 1
    id : 'a . 'a -> 'a |- id 0
      id : 'a . 'a -> 'a |- id : 'b -> 'b -| {}   <--- POINT 3

Pausing here at Point 3, when id is applied to 0, we instantiate its type variable 'a with a fresh type variable 'b. Let’s finish:
在第 3 点暂停,当 id 应用于 0 时,我们用新的类型变量 'b 实例化其类型变量 'a 。让我们结束吧:

{} |- let id = fun x -> x in (let a = id 0 in id true) : 'e -| 'b -> 'b = int -> 'c, 'd -> 'd = bool -> 'e
  {} |- fun x -> x : 'a -> 'a -| {}
    x : 'a |- x : 'a -| {}
  id : 'a . 'a -> 'a |- let a = id 0 in id true : 'e -| 'b -> 'b = int -> 'c, 'd -> 'd = bool -> 'e   <--- POINT 1
    id : 'a . 'a -> 'a |- id 0 : 'c -| 'b -> 'b = int -> 'c
      id : 'a . 'a -> 'a |- id : 'b -> 'b -| {}   <--- POINT 3
      id : 'a . 'a -> 'a |- 0 : int -| {}
    id : 'a . 'a -> 'a, a : 'b |- id true : 'e -| 'd -> 'd = bool -> 'e   <--- POINT 2
      id : 'a . 'a -> 'a, a : 'b |- id : 'd -> 'd -| {}   <--- POINT 4
      id : 'a . 'a -> 'a, a : 'b |- true : bool -| {}

At Point 4, when id is applied to true, we again instantiate its type variable 'a with a fresh type variable, this time 'd. So the constraints collected at Points 1 and 2 are no longer contradictory, because they are talking about different type variables. Those constraints are:
在第 4 点,当 id 应用于 true 时,我们再次使用新的类型变量实例化其类型变量 'a ,这次是 'd .因此,在第 1 点和第 2 点收集的约束不再矛盾,因为它们讨论的是不同类型的变量。这些限制是:

'b -> 'b = int -> 'c
'd -> 'd = bool -> 'e

The unification algorithm will therefore conclude:
因此,统一算法将得出结论:

'b = int
'c = int
'd = bool
'e = bool

So the entire expression is successfully inferred to have type bool.
因此整个表达式被成功推断为类型 bool

Instantiation and Generalization. We used two new functions, instantiate and generalize, to define type inference for let expressions. We need to define those functions.
实例化和泛化。我们使用两个新函数 instantiategeneralize 来定义 let 表达式的类型推断。我们需要定义这些函数。

The easy one is instantiate. Given a type scheme 'a1 'a2 ... 'an . t, we instantiate it by:
最简单的是 instantiate 。给定一个类型方案 'a1 'a2 ... 'an . t ,我们通过以下方式实例化它:

  • choosing n fresh type variables, and
    选择 n 新类型变量,并且

  • substituting each of those for 'a1 through 'an in t.
    将其中的每一个替换为 t 中的 'a1'an

Substitution is uncomplicated here, compared to how it was for evaluation in the substitution model, because there is nothing in a type that can bind variable names.
与替换模型中的评估方式相比,这里的替换并不复杂,因为类型中没有任何内容可以绑定变量名称。

But generalize requires more work. Here’s the let rule again:
generalize 需要更多的工作。这是 let 规则:

env |- let x = e1 in e2 : t2 -| C1, C2
  if env |- e1 : t1 -| C1
  and generalize (C1, env, x : t1) |- e2 : t2 -| C2

To generalize t1, we do the following.
为了概括 t1 ,我们执行以下操作。

First, we pretend like e1 is all that matters, and that the rest of the let expression doesn’t exist. If e1 were the entire program, how would we finish type inference? We’d run the unification algorithm on C1, get a substitution S, and return t1 S as the inferred type of e1. So, do that now. Let’s call that inferred type u1. Let’s also apply S to env to get a new environment env1, which now reflects all the type information we’ve gleaned from e1.
首先,我们假装 e1 才是最重要的,而 let 表达式的其余部分不存在。如果 e1 是整个程序,我们如何完成类型推断?我们对 C1 运行统一算法,获得替换 S ,并返回 t1 S 作为 e1 的推断类型。所以,现在就这样做。我们将该推断类型称为 u1 。我们还将 S 应用于 env 以获取新环境 env1 ,它现在反映了我们从 e1 收集的所有类型信息。

Second, we figure out which type variables in u1 should be generalized. Why not all of them? Because some type variables could have been introduced by code that surrounds the let expression, e.g.,
其次,我们找出 u1 中的哪些类型变量应该被泛化。为什么不是全部?因为某些类型变量可能是由 let 表达式周围的代码引入的,例如,

fun x ->
  (let y = e1 in e2) (let z = e3 in e4)

The type variable for x should not be generalized in inferring the type of either y or z, because x has to have the same type in all four subexpressions, e1 through e4. Generalizing could mistakenly allow x to have one type in e1 and e2, but a different type in e3 and e4.
x 的类型变量不应在推断 yz 的类型时进行泛化,因为 x 必须具有相同的类型输入所有四个子表达式 e1e4 。泛化可能会错误地允许 xe1e2 中具有一种类型,但在 e3e4

So instead we generalize only variables that are in u1 but are not in env1. That way we generalize only the type variables from e1, not variables that were already in the environment when we started inferring the let expression’s type. Suppose those variables are 'a1 ... 'an. The type scheme we give to x is then 'a1 ... 'an . u1.
因此,我们只概括 u1 中但不在 env1 中的变量。这样,我们只概括 e1 中的类型变量,而不是我们开始推断 let 表达式类型时环境中已经存在的变量。假设这些变量是 'a1 ... 'an 。我们给 x 的类型方案是 'a1 ... 'an . u1

Putting all that together, we end up with:
将所有这些放在一起,我们最终得到:

generalize(C1, env, x : t1) =
  env1, x : 'a1 ... 'an . u1

Returning to our example with the identify function from above, we had generalize({}, {}, x : 'a -> 'a). In that rather simple case, unify discovers no new equalities from the environment, so u1 = 'a -> 'a and env1 = {}. The only type variable in u1 is 'a, and it doesn’t appear in env1. So 'a is generalized, yielding 'a . 'a -> 'a as the type scheme for id.
回到上面的识别函数示例,我们有 generalize({}, {}, x : 'a -> 'a) 。在这个相当简单的情况下, unify 从环境中没有发现新的相等性,因此 u1 = 'a -> 'aenv1 = {}u1 中唯一的类型变量是 'a ,它不会出现在 env1 中。因此 'a 被泛化,产生 'a . 'a -> 'a 作为 id 的类型方案。

9.6.6. Polymorphism and Mutability
9.6.6. 多态性和可变性 ¶

There is yet one more complication to type inference for let expressions. It appears when we add mutable references to the language. Consider this example code, which does not type check in OCaml:
let 表达式的类型推断还有一个复杂之处。当我们向语言添加可变引用时,它就会出现。考虑这个示例代码,它没有在 OCaml 中进行类型检查:

let succ = fun x -> ( + ) 1 x;;
let id = fun x -> x;;
let r = ref id;;
r := succ;;
!r true;;  (* error *)

It’s clear we should infer succ : int -> int and id : 'a . 'a -> 'a. But what should the type of r be? It’s tempting to say we should infer r : 'a . ('a -> 'a) ref. That would let us instantiate the type of r to be (int -> int) ref on line 4 and store succ in r. But it also would let us instantiate the type of r to be (bool -> bool) ref on line 5. That’s a disaster: it causes the application of succ to true, which is not type safe.
很明显,我们应该推断 succ : int -> intid : 'a . 'a -> 'a 。但是 r 的类型应该是什么?我们很容易认为我们应该推断 r : 'a . ('a -> 'a) ref 。这将使我们在第 4 行将 r 的类型实例化为 (int -> int) ref 并将 succ 存储在 r 中。但它也可以让我们在第 5 行将 r 的类型实例化为 (bool -> bool) ref 。这是一场灾难:它导致将 succ 应用到 true ,这不是类型安全的。

The solution adopted by OCaml and related languages is called the value restriction: the type system is designed to prevent a polymorphic mutable value from ever holding more than one type. Let’s redo some of that example again, pausing to look at the toplevel output:
OCaml 和相关语言采用的解决方案称为值限制:类型系统旨在防止多态可变值持有多个类型。让我们再次重做该示例中的一些内容,停下来看看顶级输出:

# let id = fun x -> x;;
val id : 'a -> 'a = <fun>   (* as expected *)

# let r = ref id;;
val r : ('_weak1 -> '_weak1) ref = { ... }   (* what is _weak? *)

# r;;
- : ('_weak1 -> '_weak1) ref = { ... }   (* it's consistent at least *)

# r := succ;;
- : unit = ()

# r;;
- : (int -> int) ref = { ... }   (* did r just change type ?! *)

When the type of r is inferred, OCaml gives it a type involving a weak type variable. All such variables have a name starting with '_weak. A weak type variable is one that has not been generalized hence cannot be instantiated on multiple types. Rather, it indicates a single type that is not yet known. Think of it as type inference for that variable is not yet finished: OCaml is waiting for more information to pin down precisely what it is. When r := succ is executed, that information finally becomes available. OCaml infers that '_weak1 = int from the type of succ. Then OCaml replaces '_weak1 with int everywhere. That’s what yields an error on the final line:
当推断出 r 的类型时,OCaml 会为其提供一个涉及弱类型变量的类型。所有此类变量的名称均以 '_weak 开头。弱类型变量是一种尚未泛化的变量,因此无法在多种类型上实例化。相反,它表示一种尚不为人所知的单一类型。可以将其视为该变量的类型推断尚未完成:OCaml 正在等待更多信息来准确确定它是什么。当执行 r := succ 时,该信息最终变得可用。 OCaml 从 succ 的类型推断出 '_weak1 = int 。然后 OCaml 将所有地方的 '_weak1 替换为 int 。这就是最后一行产生错误的原因:

# !r true;;
Error: This expression has type bool but an expression was expected of type int

Since r : (int -> int) ref, we cannot apply !r to a bool.
由于 r : (int -> int) ref ,我们不能将 !r 应用于 bool

We won’t cover the implementation of weak type variables here.
这里我们不会介绍弱类型变量的实现。

But, let’s not leave this topic of the interaction between polymorphic types and mutability yet. You might be tempted to think that it’s a phenomenon that affects only OCaml. But indeed, even Java suffers.
但是,我们暂时不要离开多态类型和可变性之间的相互作用这个主题。您可能会认为这是一种仅影响 OCaml 的现象。但事实上,即使是 Java 也受到了影响。

Consider the following class hierarchy:
考虑以下类层次结构:

class Animal { }
class Elephant extends Animal { }
class Rabbit extends Animal { }

Now suppose we create an array of animals:
现在假设我们创建了一系列动物:

Animal[] a= new Rabbit[2]

Here we are using subtype polymorphism to assign an array of Rabbit objects to an Animal[] reference. That’s not the same as parametric polymorphism as we’ve been using in OCaml, but it’s nonetheless polymorphism.
在这里,我们使用子类型多态性将 Rabbit 对象数组分配给 Animal[] 引用。这与我们在 OCaml 中使用的参数多态性不同,但它仍然是多态性。

What if we try this?
如果我们尝试一下会怎样?

a[0]= new Elephant()

Since a is typed as an Animal array, it stands to reason that we could assign an elephant object into it, just as we could assign a rabbit object. And indeed that code is fine according to the Java compiler. But Java gives us a runtime error if we run that code!
由于 a 被键入为 Animal 数组,因此我们可以将大象对象分配给它,就像我们可以分配兔子对象一样。事实上,根据 Java 编译器的判断,该代码没有问题。但是如果我们运行该代码,Java 会给我们一个运行时错误!

Exception java.lang.ArrayStoreException

The problem is that mutating the first array element to be a rabbit would leave us with a Rabbit array in which one element is a Elephant. (Ouch! An elephant would sit on a rabbit. Poor bun bun.) But in Java, the type of every object of an array is supposed to be a property of the array as a whole. Every element of the array created by new Rabbit[2] therefore must be a Rabbit. So Java prevents the assignment above by detecting the error at run time and raising an exception.
问题在于,将第一个数组元素转变为兔子会给我们留下一个 Rabbit 数组,其中一个元素是 Elephant 。 (哎呀!大象会坐在兔子身上。可怜的包子。)但是在 Java 中,数组中每个对象的类型都应该是整个数组的属性。因此 new Rabbit[2] 创建的数组的每个元素都必须是 Rabbit 。因此,Java 通过在运行时检测错误并引发异常来防止上述分配。

This is really the value restriction in another guise! The type of a value stored in a mutable location may not change, according to the value restriction. With arrays, Java implements that with a run-time check, instead of rejecting the program at compile time. This strikes a balance between soundness (preventing errors from happening) and expressivity (allowing more error-free programs to type check).
这实在是另一种形式的价值限制!根据值限制,存储在可变位置的值的类型可能不会改变。对于数组,Java 通过运行时检查来实现这一点,而不是在编译时拒绝程序。这在健全性(防止错误发生)和表达性(允许更多无错误的程序进行类型检查)之间取得了平衡。

9.7. Summary 9.7. 小结 ¶

At first it might seem mysterious how a programming language could be implemented. But, after this chapter, hopefully some of that mystery has been revealed. Implementation of a programming language is just a matter of the same studious application of syntax, dynamic semantics, and static semantics that we’ve studied throughout this book. It also relies heavily on CS theory of the kind studied in discrete mathematics or theory of computation courses.
乍一看,如何实现编程语言似乎很神秘。但是,在本章之后,希望一些谜团能够被揭开。编程语言的实现只是我们在本书中学习的语法、动态语义和静态语义的精心应用的问题。它还在很大程度上依赖于离散数学或计算理论课程中研究的 CS 理论。

9.7.1. Terms and Concepts
9.7.1. 术语和概念 ¶

  • abstract syntax 抽象语法

  • abstract syntax tree 抽象语法树

  • associativity

  • back end 后端

  • Backus-Naur Form (BNF) 巴科斯-诺尔范式 (BNF)

  • big step 迈出一大步

  • bytecode 字节码

  • call by name 叫名字

  • call by value 按值调用

  • capture-avoiding substitution
    避免捕获取代

  • closure 关闭

  • compiler 编译器

  • concrete syntax 具体语法

  • constraint 约束

  • context-free grammar 上下文无关语法

  • context-free language 上下文无关语言

  • desugaring 脱糖

  • dynamic environment 动态环境

  • dynamic scope 动态范围

  • environment model 环境模型

  • evaluation 评估

  • fresh 新鲜的

  • front end 前端

  • generalization 概括

  • Hindley–Milner (HM) type inference algorithm
    Hindley–Milner (HM) 类型推理算法

  • implicit typing 隐式类型

  • instantiation 实例化

  • intermediate representation
    中间表示

  • interpreter 口译员

  • lambda calculus 拉姆达演算

  • let polymorphism 让多态性

  • lexer 词法分析器

  • machine configuration 机器配置

  • metavariable 元变量

  • nonterminal 非终结符

  • operational semantics 操作语义

  • optimizing compiler 优化编译器

  • parser 解析器

  • precedence 优先级

  • preliminary type variable
    初步类型变量

  • preservation 保存

  • primitive operatiohn 原始操作

  • progress 进步

  • pushdown automata 下推自动机

  • regular expression 正则表达式

  • regular language 常规语言

  • relation 关系

  • semantic analysis 语义分析

  • short circuit 短路

  • small step 小步

  • source program 源程序

  • static scope 静态范围

  • static typing 静态类型

  • stuck 卡住

  • substitution 代换

  • substitution model 替代模型

  • symbol 象征

  • symbol table 符号表

  • target program 目标计划

  • terminal 终端

  • token 代币

  • type annotation 类型注释

  • type checking 类型检查

  • type inference 类型推断

  • type reconstruction 类型重构

  • type safety 类型安全

  • type scheme 类型方案

  • type system 类型系统

  • type variable 类型变量

  • typing context 打字上下文

  • unification 统一

  • unifier 统一者

  • value 价值

  • value restriction 值限制

  • virtual machine 虚拟机

  • weak type variable 弱类型变量

  • well typed 打字好

9.7.2. Further Reading 9.7.2. 延伸阅读 ¶

  • Types and Programming Languages by Benjamin C. Pierce, chapters 1-14, 22.
    Benjamin C. Pierce 的《类型和编程语言》,第 1-14、22 章。

  • Modern Compiler Implementation (in Java or ML) by Andrew W. Appel, chapters 1-5.
    现代编译器实现(Java 或 ML),作者:Andrew W. Appel,第 1-5 章。

  • Automata and Computability by Dexter C. Kozen, chapters 1-27.
    自动机和可计算性,作者 Dexter C. Kozen,第 1-27 章。

  • Real World OCaml has a chapter on the OCaml frontend.
    现实世界 OCaml 有一个关于 OCaml 前端的章节。

  • This webpage documents some of the internals of the OCaml type checker and inferencer.
    该网页记录了 OCaml 类型检查器和推理器的一些内部结构。

  • The OCaml VM aka the Zinc Machine is described in these papers: 1, 2.
    OCaml VM 又名 Zinc Machine 在以下论文中进行了描述:1、2。

9.7.3. Acknowledgment 9.7.3. 致谢 ¶

Our treatment of type inference is based on Pierce.
我们对类型推断的处理是基于皮尔斯的。

9.8. Exercises 9.8. 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。

Many of these exercises rely on the SimPL interpreter as starter code. You can download it here: simpl.zip.
其中许多练习都依赖 SimPL 解释器作为起始代码。您可以在这里下载:simpl.zip。


Exercise: parse [★] 练习:解析[★]

Run make utop in the SimPL interpreter implementation. It will compile the interpreter and launch utop. Then type open Interp.Main and evaluate the following expressions. Note what each returns.
在 SimPL 解释器实现中运行 make utop 。它将编译解释器并启动 utop。然后输入 open Interp.Main 并计算以下表达式。请注意每个返回的内容。

  • parse "22"

  • parse "1 + 2 + 3"

  • parse "let x = 2 in 20 + x"

Also evaluate these expressions, which will raise exceptions. Explain why each one is an error, and whether the error occurs during parsing or lexing.
还要评估这些表达式,这将引发异常。解释为什么每个错误都是错误,以及错误是否发生在解析或词法分析期间。

  • parse "3.14"

  • parse "3+"


Exercise: simpl ids [★★] 练习:简单的 ids [★★]

Examine the definition of the id regular expression in the SimPL lexer. Identify at least one way in which it differs from the definition of OCaml identifiers.
检查 SimPL 词法分析器中 id 正则表达式的定义。识别至少一种与 OCaml 标识符定义不同的方式。


Exercise: times parsing [★★]
练习:时间解析[★★]

In the SimPL parser, the TIMES token is declared as having higher precedence than PLUS, and as being left associative. Let’s experiment with other choices.
在 SimPL 解析器中, TIMES 标记被声明为具有比 PLUS 更高的优先级,并且是左关联的。让我们尝试一下其他选择。

  • Evaluate parse "1*2*3". Note the AST. Now change the declaration of the associativity of TIMES in parser.mly to be %right instead of %left. Recompile and reevaluate parse "1*2*3". How did the AST change? Before moving on, restore the declaration to be %left.
    评估 parse "1*2*3" 。注意 AST。现在将 parser.mlyTIMES 的关联性声明更改为 %right 而不是 %left 。重新编译并重新评估 parse "1*2*3" 。 AST 有何变化?在继续之前,将声明恢复为 %left

  • Evaluate parse "1+2*3". Note the AST. Now swap the declaration %left TIMES in parser.mly with the declaration %left PLUS. Recompile and reevaluate parse "1+2*3". How did the AST change? Before moving on, restore the original declaration order.
    评估 parse "1+2*3" 。注意 AST。现在将 parser.mly 中的声明 %left TIMES 与声明 %left PLUS 交换。重新编译并重新评估 parse "1+2*3" 。 AST 有何变化?在继续之前,恢复原来的申报顺序。


Exercise: infer [★★] 练习:推断[★★]

Type inference for SimPL can be done in a much simpler way than for the larger language (with anonymous functions and let expression) that we considered in the section on type inference.
SimPL 的类型推断可以通过比我们在类型推断部分中考虑的更大语言(使用匿名函数和 let 表达式)简单得多的方式完成。

Run make in the SimPL interpreter implementation. It will compile the interpreter and launch utop. Now, define a function infer : string -> typ such that infer s parses s into an expression and infers the type of s in the empty context. Your solution will make use of the typeof function. You don’t need constraint collection or unification.
在 SimPL 解释器实现中运行 make 。它将编译解释器并启动 utop。现在,定义一个函数 infer : string -> typ ,以便 infer ss 解析为表达式,并在空上下文中推断 s 的类型。您的解决方案将使用 typeof 函数。您不需要约束收集或统一。

Try out your infer function on these test cases:
在这些测试用例上尝试您的 infer 函数:

  • "3110"

  • "1 <= 2"

  • "let x = 2 in 20 + x"


Exercise: subexpression types [★★]
练习:子表达式类型[★★]

Suppose that a SimPL expression is well typed in a context ctx. Are all of its subexpressions also well typed in ctx? For every subexpression, does there exist some context in which the subexpression is well typed? Why or why not?
假设 SimPL 表达式在上下文 ctx 中类型正确。它的所有子表达式在 ctx 中的类型是否正确?对于每个子表达式,是否存在子表达式类型正确的上下文?为什么或者为什么不?


Exercise: typing [★★] 练习:类型显示[★★]

Use the SimPL type system to show that {} |- let x = 0 in if x <= 1 then 22 else 42 : int.
使用 SimPL 类型系统来显示 {} |- let x = 0 in if x <= 1 then 22 else 42 : int


Exercise: substitution [★★]
练习:替换[★★]

What is the result of the following substitutions?
下列替换的结果是什么?

  • (x + 1){2/x}

  • (x + y){2/x}{3/y}

  • (x + y){1/z}

  • (let x = 1 in x + 1){2/x}

  • (x + (let x=1 in x+1)){2/x}

  • ((let x=1 in x+1) + x){2/x}

  • (let x=y in x+1){2/y}

  • (let x=x in x+1){2/x}


Exercise: step expressions [★]
练习:步表达式[★]

Here is an example of evaluating an expression:
以下是计算表达式的示例:

  7+5*2
-->  (step * operation)
  7+10
-->  (step + operation)
  17

There are two steps in that example, and we’ve annotated each step with a parenthetical comment to hint at which evaluation rule we’ve used. We stopped evaluating when we reached a value.
该示例中有两个步骤,我们用括号注释对每个步骤进行了注释,以暗示我们使用了哪个评估规则。当达到某个值时,我们就停止评估。

Evaluate the following expressions using the small-step substitution model. Use the “long form” of evaluation that we demonstrated above, in which you provide a hint as to which rule is applied at each step.
使用小步替换模型评估以下表达式。使用我们上面演示的评估的“长形式”,您可以在其中提供有关每个步骤应用哪个规则的提示。

  • (3 + 5) * 2 (2 steps)
    (3 + 5) * 2 (2步)

  • if 2 + 3 <= 4 then 1 + 1 else 2 + 2 (4 steps)
    if 2 + 3 <= 4 then 1 + 1 else 2 + 2 (4步)


Exercise: step let expressions [★★]
练习:步 let 表达式[★★]

Evaluate these expressions, again using the “long form” from the previous exercise.
再次使用上一个练习中的“长形式”来评估这些表达式。

  • let x = 2 + 2 in x + x (3 steps)
    let x = 2 + 2 in x + x (3步)

  • let x = 5 in ((let x = 6 in x) + x) (3 steps)
    let x = 5 in ((let x = 6 in x) + x) (3步)

  • let x = 1 in (let x = x + x in x + x) (4 steps)
    let x = 1 in (let x = x + x in x + x) (4步)


Exercise: variants [★] 练习:变体[★]

Evaluate these Core OCaml expressions using the small-step substitution model:
使用小步替换模型评估这些核心 OCaml 表达式:

  • Left (1+2) (1 step)
    Left (1+2) (1 步)

  • match Left 42 with Left x -> x+1 | Right y -> y-1 (2 steps)
    match Left 42 with Left x -> x+1 | Right y -> y-1 (2步)


Exercise: application [★★]
练习:应用[★★]

Evaluate these Core OCaml expressions using the small-step substitution model:
使用小步替换模型评估这些核心 OCaml 表达式:

  • (fun x -> 3 + x) 2 (2 steps)
    (fun x -> 3 + x) 2 (2步)

  • let f = (fun x -> x + x) in (f 3) + (f 3) (6 steps)
    let f = (fun x -> x + x) in (f 3) + (f 3) (6步)

  • let f = fun x -> x + x in let x = 1 in let g = fun y -> x + f y in g 3 (7 steps)
    let f = fun x -> x + x in let x = 1 in let g = fun y -> x + f y in g 3 (7 个步骤)

  • let f = (fun x -> fun y -> x + y) in let g = f 3 in (g 1) + (f 2 3) (9 steps)
    let f = (fun x -> fun y -> x + y) in let g = f 3 in (g 1) + (f 2 3) (9步)


Exercise: omega [★★★] 运动:自嵌[★★★]

Try evaluating (fun x -> x x) (fun x -> x x). This expression, which is usually called Ω, doesn’t type check in real OCaml, but we can still use the Core OCaml small-step semantics on it.
尝试评估 (fun x -> x x) (fun x -> x x) 。这个表达式通常称为 Ω ,在真正的 OCaml 中不会进行类型检查,但我们仍然可以在其上使用 Core OCaml 小步语义。


Exercise: pair parsing [★★★]
练习:解析对[★★★]

Add pairs (i.e., tuples with exactly two components) to SimPL. Implement lexing and parsing of pairs. Assume that the parentheses around the pair are required (not optional, as they sometimes are in OCaml). Follow this strategy:
将对(即仅具有两个分量的元组)添加到 SimPL。实现对的词法分析和解析。假设该对两边的括号是必需的(不是可选的,因为它们有时在 OCaml 中)。遵循这个策略:

  • Add a constructor for pairs to the expr type.
    将对的构造函数添加到 expr 类型。

  • Add a comma token to the parser.
    将逗号标记添加到解析器。

  • Implement lexing the comma token.
    实施逗号标记的词法分析。

  • Implement parsing of pairs.
    实现对的解析。

When you compile, you will get some inexhaustive pattern match warnings, because you have not yet implemented type checking nor interpretation of pairs. But you can still try parsing them in utop with the parse function.
编译时,您将收到一些不详尽的模式匹配警告,因为您尚未实现类型检查或对的解释。但您仍然可以尝试使用 parse 函数在 utop 中解析它们。


Exercise: pair type checking [★★★]
练习:检查对类型[★★★]

Implement type checking of pairs. Follow this strategy:
实施对的类型检查。遵循这个策略:

  • Write down a new typing rule before implementing any code.
    在实现任何代码之前写下新的打字规则。

  • Add a new constructor for pairs to the typ type.
    typ 类型添加一个新的构造函数。

  • Add a new branch to typeof.
    将新分支添加到 typeof


Exercise: pair evaluation [★★★]
练习:评估对[★★★]

Implement evaluation of pairs. Follow this strategy:
实行结对评价。遵循这个策略:

  • Implement is_value for pairs. A pair of values (e.g., (0,1)) is itself a value, so the function will need to become recursive.
    成对实现 is_value 。一对值(例如 (0,1) )本身就是一个值,因此该函数需要递归。

  • Implement subst for pairs: (e1, e2){v/x} = (e1{v/x}, e2{v/x}).

  • Implement small-step and big-step evaluation of pairs, using these rules:
    使用以下规则实现对的小步和大步评估:

(e1, e2) --> (e1', e2)
  if e1 --> e1'

(v1, e2) --> (v1, e2')
  if e2 --> e2'

(e1, e2) ==> (v1, v2)
  if e1 ==> v1
  and e2 ==> v2

Exercise: desugar list [★]
练习:脱糖清单[★]

Suppose we treat list expressions like syntactic sugar in the following way:
假设我们按以下方式将列表表达式视为语法糖:

  • [] is syntactic sugar for Left 0.
    []Left 0 的语法糖。

  • e1 :: e2 is syntactic sugar for Right (e1, e2).
    e1 :: e2Right (e1, e2) 的语法糖。

What is the core OCaml expression to which [1; 2; 3] desugars?
[1; 2; 3] 脱糖的核心 OCaml 表达式是什么?


Exercise: list not empty [★★]
练习:列表不空[★★]

Write a core OCaml function not_empty that returns 1 if a list is non-empty and 0 if the list is empty. Use the substitution model to check that your function behaves properly on these test cases:
编写一个核心 OCaml 函数 not_empty ,如果列表非空,则返回 1 ;如果列表为空,则返回 0 。使用替换模型检查您的函数在这些测试用例上的行为是否正确:

  • not_empty []

  • not_empty [1]


Exercise: generalize patterns [★★★★]
练习:归纳模式[★★★★]

In core OCaml, there are only two patterns: Left x and Right x, where x is a variable name. But in full OCaml, patterns are far more general. Let’s see how far we can generalize patterns in core OCaml.
在核心 OCaml 中,只有两种模式: Left xRight x ,其中 x 是变量名称。但在完整的 OCaml 中,模式要通用得多。让我们看看我们能在多大程度上概括核心 OCaml 中的模式。

Step 1: Here is a BNF grammar for patterns, and slightly revised BNF grammar for expressions:
步骤 1:这是模式的 BNF 语法,以及表达式的 BNF 语法稍作修改:

p ::= i | (p1, p2) | Left p | Right p | x | _

e ::= ...
    | match e with | p1 -> e1 | p2 -> e2 | ... | pn -> en

In the revised syntax for match, only the very first | on the line, immediately before the keyword match, is meta-syntax. The remaining four | on the line are syntax. Note that we require | before the first pattern.
match 的修订语法中,只有该行中紧邻关键字 match 之前的第一个 | 是元语法。剩下的四个 | 行是语法。请注意,我们在第一个模式之前需要 |

Step 2: A value v matches a pattern p if by substituting any variables or wildcards in p with values, we can obtain exactly v. For example:
步骤 2:值 v 与模式 p 匹配,如果通过用值替换 p 中的任何变量或通配符,我们可以准确地获得 v .例如:

  • 2 matches x because x{2/x} is 2.
    2 匹配 x 因为 x{2/x}2

  • Right(0,Left 0) matches Right(x,_) because Right(x,_){0/x}{Left 0/_} is Right(0,Left 0).
    Right(0,Left 0) 匹配 Right(x,_) 因为 Right(x,_){0/x}{Left 0/_}Right(0,Left 0)

Let’s define a new ternary relation called matches, guided by those examples:
让我们在这些示例的指导下定义一个名为 matches 的新三元关系:

v =~ p // s

Pronounce this relation as “v matches p producing substitutions s.”
将此关系发音为“ v 匹配 p 产生替换 s ”。

Here, s is a sequence of substitutions, such as {0/x}{Left 3/y}{(1,2)/z}. There is just a single rule for this relation:
这里, s 是一系列替换,例如 {0/x}{Left 3/y}{(1,2)/z} 。这种关系只有一条规则:

v =~ p // s
  if v = p s

For example, 例如,

2 =~ x // {2/x}
  because 2 = x{2/x}

Step 3: To evaluate a match expression:
步骤 3:计算匹配表达式:

  • Evaluate the expression being matched to a value.
    评估与值匹配的表达式。

  • If that expression matches the first pattern, evaluate the expression corresponding to that pattern.
    如果该表达式与第一个模式匹配,则计算与该模式对应的表达式。

  • Otherwise, match against the second pattern, the third, etc.
    否则,匹配第二个模式、第三个模式等。

  • If none of the patterns matches, evaluation is stuck: it cannot take any more steps.
    如果没有任何模式匹配,则评估将被卡住:无法采取更多步骤。

Using those insights, complete the following evaluation rules by filling in the places marked with ???:
利用这些见解,通过填写标有 ??? 的位置来完成以下评估规则:

(* This rule should implement evaluation of e. *)
match e with | p1 -> e1 | p2 -> e2 | ... | pn -> en
--> ???
  if ???

(* This rule implements moving past p1 to the next pattern. *)
match v with | p1 -> e1 | p2 -> e2 | ... | pn -> en
--> match v with | p2 -> e2 | ... | pn -> en
  if there does not exist an s such that ???

(* This rule implements matching v with p1 then proceeding to evaluate e1. *)
match v with | p1 -> e1 | p2 -> e2 | ... | pn -> en
--> ??? (* something involving e1 *)
  if ???

Note that we don’t need to write the following rule explicitly:
请注意,我们不需要显式地编写以下规则:

match v with |  -/->

Evaluation will get stuck at that point because none of the three other rules above will apply.
评估将在此时陷入困境,因为上述其他三个规则均不适用。

Step 4: Double check your rules by evaluating the following expression:
步骤 4:通过计算以下表达式来仔细检查您的规则:

match (1 + 2, 3) with | (1,0) -> 4 | (1,x) -> x | (x,y) -> x + y


Exercise: let rec [★★★★] 练习: let 递归[★★★★]

One of the evaluation rules for let is
let 的评估规则之一是

let x = v in e --> e{v/x}

We could try adapting that to let rec:
我们可以尝试将其调整为 let rec

let rec x = v in e --> e{v/x}   (* broken *)

But that rule doesn’t work properly, as we see in the following example:
但该规则无法正常工作,如以下示例所示:

  let rec fact = fun x ->
	if x <= 1 then 1 else x * (fact (x - 1)) in
  fact 3

-->

  (fun x -> if x <= 1 then 1 else x * (fact (x - 1)) 3

-->

  if 3 <= 1 then 1 else 3 * (fact (3 - 1))

-->

  3 * (fact (3 - 1))

-->

  3 * (fact 2)

-/->

We’re now stuck, because we need to evaluate fact, but it doesn’t step. In essence, the semantic rule we used “forgot” the function value that should have been associated with fact.
我们现在陷入困境,因为我们需要评估 fact ,但它不执行。本质上,我们使用的语义规则“忘记”了应该与 fact 关联的函数值。

A good way to fix this problem is to introduce a new language construct for recursion called simply rec. (Note that OCaml does not have any construct that corresponds directly to rec.) Formally, we extend the syntax for expressions as follows:
解决此问题的一个好方法是引入一种新的递归语言结构,简称为 rec 。 (请注意,OCaml 没有任何直接对应于 rec 的构造。)正式地,我们扩展表达式的语法如下:

e ::= ...
    | rec f -> e

and add the following evaluation rule:
并添加以下评价规则:

rec f -> e  -->  e{(rec f -> e)/f}

The intuitive reading of this rule is that when evaluating rec f -> e, we “unfold” f in the body of e. For example, here is an infinite loop coded with rec:
这条规则的直观解读是,在评估 rec f -> e 时,我们在 e 的主体中“展开” f 。例如,这是一个用 rec 编码的无限循环:

  rec f -> f

-->  (* step rec *)

  f{(rec f -> f)/f}

= (* substitute *)

  rec f -> f

--> (* step rec *)

  f{(rec f -> f)/f}

...

Now we can use rec to implement let rec. Anywhere let rec appears in a program:
现在我们可以使用 rec 来实现 let rec 。程序中出现 let rec 的任何位置:

let rec f = e1 in e2

we desugar (i.e., rewrite) it to
我们将其脱糖(即重写)为

let f = rec f -> e1 in e2

Note that the second occurrence of f (inside the rec) shadows the first one. Going back to the fact example, its desugared version is
请注意,第二次出现的 f (在 rec 内)会遮盖第一个出现的情况。回到 fact 示例,它的脱糖版本是

let fact = rec fact -> fun x ->
  if x <= 1 then 1 else x * (fact (x - 1)) in
fact 3

Evaluate the following expression (17 steps, we think, though it does get pretty tedious). You may want to simplify your life by writing “F” in place of (rec fact -> fun x -> if x <= 1 then 1 else x * (fact (x-1)))
评估以下表达式(我们认为有 17 个步骤,尽管它确实变得相当乏味)。您可能想通过写“F”代替 (rec fact -> fun x -> if x <= 1 then 1 else x * (fact (x-1))) 来简化您的生活

let rec fact = fun x ->
  if x <= 1 then 1 else x * (fact (x - 1)) in
fact 3

Exercise: simple expressions [★]
练习:简单的表达式[★]

In the small-step substitution model, evaluation of an expression was rather list-like: we could write an evaluation in a linear form like e --> e1 --> e2 --> ... --> en --> v. In the big-step environment model, evaluation is instead rather tree-like: evaluations have a nested, recursive structure. Here’s an example:
在小步替换模型中,表达式的计算类似于列表:我们可以以线性形式编写计算,例如 e --> e1 --> e2 --> ... --> en --> v 。在大步环境模型中,评估是类似树的:评估具有嵌套的递归结构。这是一个例子:

<{}, (3 + 5) * 2> ==> 16          (op rule)
    because <{}, (3 + 5)> ==> 8   (op rule)
        because <{},3> ==> 3      (int const rule)
        and     <{},5> ==> 5      (int const rule)
        and 3+5 is 8
    and <{}, 2> ==> 2             (int const rule)
    and 8*2 is 16

We’ve used indentation here to show the shape of the tree, and we’ve labeled each usage of one of the semantic rules.
我们在这里使用缩进来显示树的形状,并且我们已经标记了语义规则之一的每种用法。

Evaluate the following expressions using the big-step environment model. Use the notation for evaluation that we demonstrated above, in which you provide a hint as to which rule is applied at each node in the tree.
使用大步环境模型评估以下表达式。使用我们上面演示的评估符号,您可以在其中提供有关树中每个节点应用哪个规则的提示。

  • 110 + 3*1000 hint: three uses of the constant rule, two uses of the op rule
    110 + 3*1000 提示:常量规则的三种用途,op规则的两种用途

  • if 2 + 3 < 4 then 1 + 1 else 2 + 2 hint: five uses of constant, three uses of op, one use of if(else)
    if 2 + 3 < 4 then 1 + 1 else 2 + 2 提示:常量的使用次数为 5 次,op 的使用次数为 3 次,if(else) 的使用次数为 1 次


Exercise: let and match expressions [★★]
练习:let 和 match 表达式[★★]

Evaluate these expressions, continuing to use the tree notation, and continuing to label each usage of a rule.
评估这些表达式,继续使用树表示法,并继续标记规则的每个用法。

  • let x=0 in 1 hint: one use of let, two uses of constant
    let x=0 in 1 提示:一次使用let,两次使用constant

  • let x=2 in x+1 hint: one use of let, two uses of constant, one use of op, one use of variable
    let x=2 in x+1 提示:一次使用let,两次使用常量,一次使用op,一次使用变量

  • match Left 2 with Left x -> x+1 | Right x -> x-1 hint: one use of match(left), two uses of constant, one use of op, one use of variable
    match Left 2 with Left x -> x+1 | Right x -> x-1 提示:1次使用match(left),2次使用常量,1次使用op,1次使用变量


Exercise: closures [★★] 练习:闭包[★★]

Evaluate these expressions:
评估这些表达式:

  • (fun x -> x+1) 2 hint: one use of application, one use of anonymous function, two uses of constant, one use of op, one use of variable
    (fun x -> x+1) 2 提示:1次使用应用程序,1次使用匿名函数,2次使用常量,1次使用op,1次使用变量

  • let f = fun x -> x+1 in f 2 hint: one use of let, one use of anonymous function, one use of application, two uses of variable, one use of op, two uses of constant
    let f = fun x -> x+1 in f 2 提示:一次使用let,一次使用匿名函数,一次使用应用程序,两次使用变量,一次使用op,两次使用常量


Exercise: lexical scope and shadowing [★★]
练习:词法作用域与阴影[★★]

Evaluate these expressions:
评估这些表达式:

  • let x=0 in x + (let x=1 in x) hint: two uses of let, two uses of variable, one use of op, two uses of constant
    let x=0 in x + (let x=1 in x) 提示:两次使用let,两次使用变量,一次使用op,两次使用常量

  • let x=1 in let f=fun y -> x in let x=2 in f 0 hint: three uses of let, one use of anonymous function, one use of application, two uses of variable, three uses of constant
    let x=1 in let f=fun y -> x in let x=2 in f 0 提示:let的三种使用,匿名函数的一种使用,应用程序的一种使用,变量的两种使用,常量的三种使用


Exercise: more evaluation [★★]
练习:更多评估[★★]

Evaluate these: 评估这些:

  • let x = 2 + 2 in x + x

  • let x = 1 in let x = x + x in x + x

  • let f = fun x -> fun y -> x + y in let g = f 3 in g 2

  • let f = fst ((let x = 3 in fun y -> x), 2) in f 0


Exercise: dynamic scope [★★★]
练习:动态作用域[★★★]

Use dynamic scope to evaluate the following expression. You do not need to write down all of the evaluation steps unless you find it helpful. Compare your answer to the answer you would expect from a language with lexical scope.
使用动态范围来计算以下表达式。您不需要写下所有评估步骤,除非您发现它有帮助。将您的答案与您期望从具有词法范围的语言获得的答案进行比较。

let x = 5 in
let f y = x + y in
let x = 4 in
f 3

Exercise: more dynamic scope [★★★]
练习:更多动态作用域[★★★]

Use dynamic scope to evaluate the following expressions. Compare your answers to the answers you would expect from a language with lexical scope.
使用动态范围来计算以下表达式。将您的答案与您期望从具有词法范围的语言获得的答案进行比较。

Expression 1: 表达式1:

let x = 5 in
let f y = x + y in
let g x = f x in
let x = 4 in
g 3

Expression 2: 表达式2:

let f y = x + y in
let x = 3 in
let y = 4 in
f 2

Exercise: constraints [★★]
练习:约束[★★]

Show the derivation of the env |- e : t -| C relation for these expressions:
显示这些表达式的 env |- e : t -| C 关系的推导:

1. fun x -> ( + ) 1 x
2. fun b -> if b then false else true
3. fun x -> fun y -> if x <= y then y else x

Exercise: unify [★★] 练习:统一算法[★★]

Use the unification algorithm to solve the following system of constraints. Your answer should be a substitution, in the sense that the unification algorithm defines that term.
使用统一算法求解以下约束系统。从统一算法定义该术语的意义上来说,您的答案应该是替换。

X = int
Y = X -> X

Exercise: unify more [★★★]
练习:更多统一算法[★★★]

Use the unification algorithm to solve the following system of constraints. Your answer should be a substitution, in the sense that the unification algorithm defines that term.
使用统一算法求解以下约束系统。从统一算法定义该术语的意义上来说,您的答案应该是替换。

X -> Y = Y -> Z
     Z = U -> W

Exercise: infer apply [★★★]
练习:推断应用[★★★]

Using the HM type inference algorithm, infer the type of the following definition:
使用HM类型推断算法,推断以下定义的类型:

let apply f x = f x

Remember to go through these steps:
请记住执行以下步骤:

  • desugar the definition entirely (i.e., construct an AST)
    完全脱糖定义(即构建 AST)

  • collect constraints 收集约束条件

  • solve the constraints with unification
    统一解决约束


Exercise: infer double [★★★]
练习:推断二重应用[★★★]

Using the HM type inference algorithm, infer the type of the following definition:
使用HM类型推断算法,推断以下定义的类型:

let double f x = f (f x)

Exercise: infer S [★★★★] 练习:推断 S [★★★★]

Using the HM type inference algorithm, infer the type of the following definition:
使用HM类型推断算法,推断以下定义的类型:

let s x y z = (x z) (y z)

The Curry-Howard Correspondence
库里-霍华德通讯 ¶

Note 笔记

A lagniappe is a small and unexpected gift — a little “something extra”. Please enjoy this little chapter, which contains one of the most beautiful results in the entire book. It is based on the paper Propositions as Types by Philip Wadler. You can watch an entertaining recorded lecture by Prof. Wadler on it, in addition to our lecture below.
最后的小赠品是一份小而意想不到的礼物——一点“额外的东西”。请享受这一小章,其中包含整本书中最美丽的结果之一。它基于 Philip Wadler 的论文 Propositions as Types。除了我们下面的讲座之外,您还可以观看由 Wadler 教授录制的有趣讲座。

As we observed long ago, OCaml is a language in the ML family, and ML was originally designed as the meta language for a theorem prover—that is, a computer program designed to help prove and check the proofs of logical formulas. When constructing proofs, it’s desirable to make sure that you can only prove true formulas, to make sure that you don’t make incorrect arguments, etc.
正如我们很久以前观察到的,OCaml 是 ML 家族中的一种语言,ML 最初被设计为定理证明者的元语言,即一种旨在帮助证明和检查逻辑公式证明的计算机程序。在构建证明时,最好确保您只能证明真实的公式,以确保您不会提出错误的论证,等等。

The dream would be to have a computer program that can determine the truth or falsity of any logical formula. For some formulas, that is possible. But, one of the groundbreaking results in the early 20th century was that it is not possible, in general, for a computer program to do this. Alonzo Church and Alan Turing independently showed this in 1936. Church used the lambda calculus as a model for computers; Turing used what we now call Turing machines. The Church-Turing thesis is a hypothesis that says the lambda calculus and Turing machines both formalize what “computation” informally means.
我们的梦想是拥有一个可以确定任何逻辑公式的真假的计算机程序。对于某些公式,这是可能的。但是,20 世纪初的突破性成果之一便是:一般来说,计算机程序不可能做到这一点。 Alonzo Church 和 Alan Turing 在 1936 年独立地展示了这一点。Church 使用 lambda 演算作为计算机模型;图灵使用了我们现在所说的图灵机丘奇-图灵论文是一个假设,认为 lambda 演算和图灵机都形式化了“计算”的非正式含义。

Instead of focusing on that impossible task, we’re going to focus on the relationship between proofs and programs. It turns out the two are deeply connected in a surprising way.
我们不会关注这个不可能的任务,而是关注证明和程序之间的关系。事实证明,两者以一种令人惊讶的方式紧密相连。

Computing with Evidence
有证据的计算 ¶

We’re accustomed to OCaml programs that manipulate data, such as integers and variants and functions. Those data values are always typed: at compile time, OCaml infers (or the programmer annotates) the types of expressions. For example, 3110 : int, and [] : 'a list. We long ago learned to read those as “3110 has type int”, and “[] has type 'a list”.
我们习惯使用 OCaml 程序来操作数据,例如整数、变体和函数。这些数据值始终是类型化的:在编译时,OCaml 推断(或程序员注释)表达式的类型。例如, 3110 : int[] : 'a list 。我们很久以前就学会了将它们读作“ 3110 具有类型 int ”,而“ [] 具有类型 'a list ”。

Let’s try a different reading now. Instead of “has type”, let’s read “is evidence for”. So, 3110 is evidence for int. What does that mean? Think of a type as a set of values. So, 3110 is evidence that type is not empty. Likewise, [] is evidence that the type 'a list is not empty. We say that the type is inhabited if it is not empty.
现在让我们尝试不同的阅读方式。让我们阅读“是……的证据”,而不是“有类型”。因此, 3110int 的证据。这意味着什么?将类型视为一组值。因此, 3110 是类型不为空的证据。同样, [] 是类型 'a list 不为空的证据。如果该类型不为空,我们就说该类型是有人居住的。

Are there empty types? There actually is one in OCaml, though we’ve never had reason to mention it before. It’s possible to define a variant type that has no constructors:
有空类型吗? OCaml 中实际上有一个,尽管我们之前从未有理由提及它。可以定义没有构造函数的变体类型:

type empty = |
type empty = |

We could have called that type anything we wanted instead of empty; the special syntax there is just writing | instead of actual constructors. (Note, that syntax might give some editors some trouble. You might need to put double-semicolon after it to get the formatting right.) It is impossible to construct a value of type empty, exactly because it has no constructors. So, empty is not inhabited.
我们可以将该类型称为我们想要的任何名称,而不是 empty ;那里的特殊语法只是编写 | 而不是实际的构造函数。 (请注意,该语法可能会给某些编辑器带来一些麻烦。您可能需要在其后面添加双分号才能获得正确的格式。)不可能构造 empty 类型的值,正是因为它具有没有构造函数。因此, empty 无人居住。

Under our new reading based on evidence, we could think about functions as ways to manipulate and transform evidence—just as we are already accustomed to thinking about functions as ways to manipulate and transform data. For example, the following functions construct and destruct pairs:
根据我们基于证据的新解读,我们可以将函数视为操纵和转换证据的方式,就像我们已经习惯将函数视为操纵和转换数据的方式一样。例如,以下函数构造和析构对:

let pair x y = (x, y)
let fst (x, y) = x
let snd (x, y) = y
val pair : 'a -> 'b -> 'a * 'b = <fun>
val fst : 'a * 'b -> 'a = <fun>
val snd : 'a * 'b -> 'b = <fun>

We could think of pair as a function that takes in evidence for 'a and evidence for 'b, and gives us back evidence for 'a * 'b. That latter piece of evidence is the the pair (x, y) containing the individual pieces of evidence, x and y. Similarly, fst and snd extract the individual pieces of evidence from the pair. Thus,
我们可以将 pair 视为一个函数,它接收 'a 的证据和 'b 的证据,并返回 'a * 'b 的证据。后一个证据是包含各个证据 xy 的对 (x, y) 。类似地, fstsnd 从该对中提取各个证据。因此,

  • If you have evidence for 'a and evidence for 'b, you can produce evidence for 'a and 'b.
    如果您有 'a 的证据和 'b 的证据,则可以提供 'a'b 的证据。

  • If you have evidence for 'a and 'b, then you can produce evidence for 'a.
    如果您有 'a'b 的证据,那么您可以提供 'a 的证据。

  • If you have evidence for 'a and 'b, then you can produce evidence for 'b.
    如果您有 'a'b 的证据,那么您可以提供 'b 的证据。

In learning to do proofs (say, in a discrete mathematics class), you will have learned that in order to prove two statements hold, you individually have to prove that each holds. That is, to prove the conjunction of A and B, you must prove A as well as prove B. Likewise, if you have a proof of the conjunction of A and B, then you can conclude A holds, and you can conclude B holds. We can write those patterns of reasoning as logical formulas, using /\ to denote conjunction and -> to denote implication:
在学习证明时(例如,在离散数学课上),您将了解到,为了证明两个陈述成立,您必须单独证明每个陈述成立。也就是说,要证明A和B的合取,你必须证明A并证明B。同样,如果你有A和B的合取证明,那么你可以得出A成立,也可以得出B成立。我们可以将这些推理模式写成逻辑公式,使用 /\ 表示合取,使用 -> 表示蕴涵:

A -> B -> A /\ B
A /\ B -> A
A /\ B -> B

Proofs are a form of evidence: they are logical arguments about the truth of a statement. So another reading of those formulas would be:
证明是证据的一种形式:它们是关于陈述真实性的逻辑论证。因此,这些公式的另一种解读是:

  • If you have evidence for A and evidence for B, you can produce evidence for A and B.
    如果你有 A 的证据和 B 的证据,你就可以提供 A 和 B 的证据。

  • If you have evidence for A and B, then you can produce evidence for A.
    如果你有 A 和 B 的证据,那么你就可以提供 A 的证据。

  • If you have evidence for A and B, then you can produce evidence for B.
    如果你有A和B的证据,那么你就可以提供B的证据。

Notice how we now have given the same reading for programs and for proofs. They are both ways of manipulating and transforming evidence. In fact, take a close look at the types for pair, fst, and snd compared to the logical formulas that describe valid patterns of reasoning:
请注意,我们现在如何为程序和证明提供相同的读数。它们都是操纵和转换证据的方式。事实上,仔细查看 pairfstsnd 的类型,与描述有效推理模式的逻辑公式进行比较:

val pair : 'a -> 'b -> 'a * 'b         A -> B -> A /\ B
val fst : 'a * 'b -> 'a                A /\ B -> A
val snd : 'a * 'b -> 'b                A /\ B -> B

If you replace 'a with A, and 'b with B, and * with /\, the types of the programs are identical to the formulas!
如果将 'a 替换为 A,将 'b 替换为 B,将 * 替换为 /\ ,则程序的类型与公式!

The Correspondence 通讯 ¶

What we have just discovered is that computing with evidence corresponds to constructing valid logical proofs. This correspondence is not just an accident that occurs with these three specific programs. Rather, it is a deep phenomenon that links the fields of programming and logic. Aspects of it have been discovered by many people working in many areas. So, it goes by many names. One common name is the Curry-Howard correspondence, named for logicians Haskell Curry (for whom the functional programming language Haskell is named) and William Howard. This correspondence links ideas from programming to ideas from logic:
我们刚刚发现的是,用证据进行计算相当于构建有效的逻辑证明。这种对应关系不仅仅是这三个特定程序发生的意外。相反,它是一种连接编程和逻辑领域的深层现象。许多领域的许多工作人员都发现了它的各个方面。因此,它有很多名字。一个常见的名称是 Curry-Howard 对应关系,以逻辑学家 Haskell Curry(函数式编程语言 Haskell 就是以他的名字命名)和 William Howard 命名的。此对应关系将编程思想与逻辑思想联系起来:

  • Types correspond to logical formulas (aka propositions).
    类型对应于逻辑公式(也称为命题)。

  • Programs correspond to logical proofs.
    程序对应于逻辑证明。

  • Evaluation corresponds to simplification of proofs.
    评估对应于证明的简化。

We’ve already seen the first two of those correspondences. The types of our three little programs corresponded to formulas, and the programs themselves corresponded to the reasoning done in proofs involving conjunctions. We haven’t seen the third yet; we will later.
我们已经看到了前两封信件。我们的三个小程序的类型对应于公式,程序本身对应于涉及连词的证明中进行的推理。我们还没有看到第三个;我们稍后会。

Let’s dig into each of the correspondences to appreciate them more fully.
让我们深入研究每封信件,以更全面地欣赏它们。

Types Correspond to Propositions
类型对应于命题 ¶

In propositional logic, formulas are created with atomic propositions, negation, conjunction, disjunction, and implication. The following BNF describes propositional logic formulas:
在命题逻辑中,公式是由原子命题、否定、合取、析取和蕴涵创建的。下面的 BNF 描述了命题逻辑公式:

p ::= atom
    | ~ p      (* negation *)
    | p /\ p   (* conjunction *)
    | p \/ p   (* disjunction *)
    | p -> p   (* implication *)

atom ::= <identifiers>

For example, raining /\ snowing /\ cold is a proposition stating that it is simultaneously raining and snowing and cold (a weather condition known as Ithacating). An atomic proposition might hold of the world, or not. There are two distinguished atomic propositions, written true and false, which are always hold and never hold, respectively.
例如, raining /\ snowing /\ cold 是一个命题,表示同时下雨、下雪和寒冷(称为 Ithacating 的天气条件)。一个原子命题可能适用于世界,也可能不适用。有两个不同的原子命题,分别写为 truefalse ,它们分别是始终成立和从不成立。

All these connectives (so-called because they connect formulas together) have correspondences in the types of functional programs.
所有这些连接词(之所以这样称呼是因为它们将公式连接在一起)在函数程序的类型中都有对应关系。

Conjunction. We have already seen that the /\ connective corresponds to the * type constructor. Proposition A /\ B asserts the truth of both A and B. An OCaml value of type a * b contains values both of type a and b. Both /\ and * thus correspond to the idea of pairing or products.
连词。我们已经看到 /\ 连接词对应于 * 类型构造函数。命题 A /\ B 断言 AB 均为真。 a * b 类型的 OCaml 值包含 ab 类型的值。因此, /\* 都对应于配对或产品的概念。

Implication. The implication connective -> corresponds to the function type constructor ->. Proposition A -> B asserts that if you can show that A holds, then you can show that B holds. In other words, by assuming A, you can conclude B. In a sense, that means you can transform A into B. An OCaml value of type a -> b expresses that idea even more clearly. Such a value is a function that transforms a value of type a into a value of type b. Thus, if you can show that a is inhabited (by exhibiting a value of that type), you can show that b is inhabited (by applying the function of type a -> b to it). So, -> corresponds to the idea of transformation.
含义。蕴涵连接词 -> 对应于函数类型构造函数 -> 。命题 A -> B 断言,如果您可以证明 A 成立,那么您就可以证明 B 成立。换句话说,通过假设 A ,您可以得出 B 。从某种意义上说,这意味着您可以将 A 转换为 Ba -> b 类型的 OCaml 值更清楚地表达了这一想法。这样的值是将 a 类型的值转换为 b 类型的值的函数。因此,如果您可以显示 a 已被占用(通过显示该类型的值),则可以显示 b 已被占用(通过应用 a -> b 对应的是变换的思想。

Disjunction. The disjunction connective \/ corresponds to something a little more difficult to express concisely in OCaml. Proposition A \/ B asserts that either you can show A holds or B holds. Let’s strengthen that to further assert that in addition to showing one of them holds, you have to specify which one you are showing. Why would that matter?
脱节。析取连接词 \/ 对应于在 OCaml 中更难以简洁表达的内容。命题 A \/ B 断言您可以显示 A 保留或 B 保留。让我们加强这一点,进一步断言除了显示其中一个保留之外,您还必须指定要显示哪一个。为什么这很重要?

Suppose we were working on a proof of the twin prime conjecture, an unsolved problem that states there are infinitely many twin primes (primes of the form n and n+2, such as 3 and 5, or 5 and 7). Let the atomic proposition TP denote that there are infinitely many twin primes. Then the proposition TP \/ ~ TP seems reasonable: either there are infinitely many twin primes, or there aren’t. We wouldn’t even have to figure out how to prove the conjecture! But if we strengthen the meaning of \/ to be that we have to state which one of the sides, left or right, holds, then we would either have to give a proof or disproof of the conjecture. No one knows how to do that currently. So we could not prove TP \/ ~ TP.
假设我们正在研究孪生素数猜想的证明,这是一个未解决的问题,指出存在无限多个孪生素数(形式为 nn+2 的素数,例如 3 和 5 ,或 5 和 7)。令原子命题 TP 表示存在无限多个孪生素数。那么命题 TP \/ ~ TP 似乎是合理的:要么有无限多个孪生素数,要么没有。我们甚至不需要弄清楚如何证明这个猜想!但是,如果我们将 \/ 的含义加强为我们必须声明哪一侧(左或右)成立,那么我们要么必须给出猜想的证明或反驳。目前没有人知道如何做到这一点。所以我们无法证明 TP \/ ~ TP

Henceforth we will use \/ in that stronger sense of having to identify whether we are giving a proof of the left or the right side proposition. Thus, we can’t necessarily conclude p \/ ~ p for any proposition p: it will matter whether we can prove p or ~ p on their own. Technically, this makes our propositional logic constructive rather than classical. In constructive logic we must construct the proof of the individual propositions. Classical logic (the traditional way \/ is understood) does not require that.
从今以后,我们将在更强烈的意义上使用 \/ ,即必须确定我们是在给出左侧命题的证明还是右侧命题的证明。因此,我们不一定能对任何命题 p 得出 p \/ ~ p 的结论:我们能否证明 p~ p 很重要自己的。从技术上讲,这使得我们的命题逻辑具有建设性,而不是经典的。在构造性逻辑中,我们必须构造各个命题的证明。经典逻辑(理解 \/ 的传统方式)不需要这样做。

Returning to the correspondence between disjunction and variants, consider this variant type:
回到析取和变体之间的对应关系,考虑这个变体类型:

type ('a, 'b) disj = Left of 'a | Right of 'b
type ('a, 'b) disj = Left of 'a | Right of 'b

A value v of that type is either Left a, where a : 'a; or Right b, where b : 'b. That is, v identifies (i) whether it is tagged with the left constructor or the right constructor, and (ii) carries within it exactly one sub-value of type either 'a or 'b—not two subvalues of both types, which is what 'a * 'b would be.
该类型的值 v 可以是 Left a ,其中 a : 'a ;或 Right b ,其中 b : 'b 。也就是说, v 标识 (i) 它是用左构造函数还是右构造函数标记,并且 (ii) 在其中恰好携带 'a 类型的一个子值或 'b - 不是两种类型的两个子值,这就是 'a * 'b

Thus, the (constructive) disjunction connective \/ corresponds to the disj type constructor. Proposition A \/ B asserts that either A or B holds as well as which one, left or right, it is. An OCaml value of type ('a, 'b) disj similarly contains a value of type either 'a or 'b as well as identifying (with the Left or Right constructor) which one it is. Both \/ and disj therefore correspond to the idea of unions.
因此,(构造性)析取连接词 \/ 对应于 disj 类型构造函数。命题 A \/ B 断言 AB 成立,以及它是左还是右。 ('a, 'b) disj 类型的 OCaml 值同样包含 'a'b 类型的值以及标识(使用 LeftRight 构造函数)是哪一个。因此 \/disj 都对应于联合的概念。

Truth and Falsity The atomic proposition true is the only proposition that is guaranteed to always hold. There are many types in OCaml that are always inhabited, but the simplest of all of them is unit: there is one value () of type unit. So the proposition true (best) corresponds to the type unit.
真与假原子命题 true 是唯一保证永远成立的命题。 OCaml 中有许多类型始终存在,但其中最简单的是 unit :有一个 unit 类型的值 () 。因此命题 true (最佳)对应于类型 unit

Likewise, the atomic proposition false is the only proposition that is guaranteed to never hold. That corresponds to the empty type we introduced earlier, which has no constructors. (Other names for that type could include zero or void, but we’ll stick with empty.)
同样,原子命题 false 是唯一保证永远不成立的命题。这对应于我们之前介绍的 empty 类型,它没有构造函数。 (该类型的其他名称可能包括 zerovoid ,但我们将坚持使用 empty 。)

There is a subtlety with empty that we should address. The type has no constructors, but it is nonetheless possible to write expressions that have type empty. Here is one way:
我们应该解决 empty 的一个微妙之处。该类型没有构造函数,但仍然可以编写类型为 empty 的表达式。这是一种方法:

let rec loop x = loop x
val loop : 'a -> 'b = <fun>

Now if you enter this code in utop you will get no response:
现在,如果您在 utop 中输入此代码,您将不会得到任何响应:

let e : empty = loop ()

That expression type checks successfully, then enters an infinite loop. So, there is never any value of type empty that is produced, even though the expression has that type.
该表达式类型检查成功,然后进入无限循环。因此,即使表达式具有该类型,也永远不会生成任何 empty 类型的值。

Here is another way:
这是另一种方法:

let e : empty = failwith ""
Exception: Failure "".
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from unknown location
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150

Again, the expression type checks, but it never produces an actual value of type empty. Instead, this time an exception is produced.
同样,表达式类型进行检查,但它永远不会生成 empty 类型的实际值。相反,这次产生了异常。

So the type empty is not inhabited, even though there are some expressions of that type. But, if we require programs to be total, we can rule out those expressions. That means eliminating programs that raise exceptions or go into an infinite loop. We did in fact make that requirement when we started discussing formal methods, and we will continue to assume it.
因此,类型 empty 未被占用,即使存在一些该类型的表达式。但是,如果我们要求程序是全面的,我们可以排除这些表达式。这意味着消除引发异常或进入无限循环的程序。事实上,当我们开始讨论形式化方法时,我们确实提出了这个要求,并且我们将继续假设它。

Negation. This connective is the trickiest. Let’s consider negation to actually be syntactic sugar. In particular, let’s say that the propositional formula ~ p actually means this formula instead: p -> false. Why? The formula ~ p should mean that p does not hold. So if p did hold, then it would lead to a contradiction. Thus, given p, we could conclude false. This is the standard way of understanding negation in constructive logic.
否定。这个连接词是最棘手的。让我们将否定视为语法糖。特别是,假设命题公式 ~ p 实际上意味着这个公式: p -> false 。为什么?公式 ~ p 应该意味着 p 不成立。因此,如果 p 确实成立,那么就会导致矛盾。因此,给定 p ,我们可以得出 false 的结论。这是理解构造逻辑中否定的标准方法。

Given that syntactic sugar, ~ p therefore corresponds to a function type whose return type is empty. Such a function could never actually return. Given our ongoing assumption that programs are total, that must mean it’s impossible to even call that function. So, it must be impossible to construct a value of the function’s input type. Negation therefore corresponds to the idea of impossibility, or contradiction.
鉴于该语法糖, ~ p 因此对应于返回类型为 empty 的函数类型。这样的函数永远不会真正返回。鉴于我们一直假设程序是完整的,这必定意味着甚至不可能调用该函数。因此,不可能构造函数输入类型的值。因此,否定对应于不可能性或矛盾的观念。

Propositions as types. We have now created the following correspondence that enables us to read propositions as types:
命题作为类型。我们现在创建了以下对应关系,使我们能够将命题视为类型:

  • /\ and *
    /\*

  • -> and ->
    ->->

  • \/ and disj
    \/disj

  • true and unit
    trueunit

  • false and empty
    falseempty

  • ~ and ... -> false
    ~... -> false

But that is only the first level of the Curry-Howard correspondence. It goes deeper…
但这只是库里和霍华德通信的第一层。它更深入……

Programs Correspond to Proofs
程序对应于证明 ¶

We have seen that programs and proofs are both ways to manipulate and transform evidence. In fact, every program is a proof that the type of the program is inhabited, since the type checker must verify that the program is well typed.
我们已经看到,程序和证明都是操纵和转换证据的方式。事实上,每个程序都是该程序类型已存在的证明,因为类型检查器必须验证该程序的类型是否正确。

The details of type checking, though, lead to an even more compelling correspondence between programs and proofs. Let’s restrict our attention to programs and proofs involving just conjunction and implication, or equivalently, pairs and functions. (The other propositional connectives could be included as well, but require additional work.)
然而,类型检查的细节导致程序和证明之间更加引人注目的对应关系。让我们将注意力限制在只涉及合取和蕴涵,或者等价地,对和函数的程序和证明上。 (其他命题连接词也可以包括在内,但需要额外的工作。)

Type checking rules. For type checking, we gave many rules to define when a program is well typed. Here are rules for variables, functions, and pairs:
类型检查规则。对于类型检查,我们给出了许多规则来定义程序何时是良好类型的。以下是变量、函数和对的规则:

{x : t, ...} |- x : t

A variable has whatever type the environment says it has.
变量具有环境指定的任何类型。

env |- fun x -> e : t -> t'
if env[x -> t] |- e : t'

An anonymous function fun x -> e has type t -> t' if e has type t' in a static environment extended to bind x to type t.
如果 e 在扩展为绑定 x 的静态环境中具有类型 t' ,则匿名函数 fun x -> e 具有类型 t -> t' 输入 t

env |- e1 e2 : t'
if env |- e1 : t -> t'
and env |- e2 : t

An application e1 e2 has type t' if e1 has type t -> t' and e2 has type t.
应用 e1 e2 有类型 t'e1 具有类型 t -> t' 并且 e2 具有类型 t

env |- (e1, e2) : t1 * t2
if env |- e1 : t1
and env |- e2 : t2

The pair (e1, e2) has type t1 * t2 if e1 has type t1 and e2 has type t2.
(e1, e2) 有类型 t1 * t2e1 具有类型 t1 并且 e2 具有类型 t2

env |- fst e : t1
if env |- e : t1 * t2

env |- snd e : t2
if env |- e : t1 * t2

If e has type t1 * t2, then fst e has type t1, and snd e has type t2.
如果 e 具有类型 t1 * t2 ,则 fst e 具有类型 t1 ,并且 snd e 具有类型 t2

Proof trees. Another way of expressing those rules would be to draw proof trees that show the recursive application of rules. Here are those proof trees:
证明树。表达这些规则的另一种方法是绘制证明树,以显示规则的递归应用。这是那些证明树:

---------------------
{x : t, ...} |- x : t


   env[x -> t1] |- e2 : t2
-----------------------------
env |- fun x -> e2 : t1 -> t2


env |- fun x -> e2 : t1 -> t2        env |- e1 : t1
---------------------------------------------------
          env |- (fun x -> e2) e1 : t2


env |- e1 : t1         env |- e2 : t2
-------------------------------------
     env |- (e1, e2) : t1 * t2


 env |- e : t1 * t2
--------------------
 env |- fst e : t1


 env |- e : t1 * t2
--------------------
 env |- snd e : t2

Proof trees, logically. Let’s rewrite each of those proof trees to eliminate the programs, leaving only the types. At the same time, let’s use the propositions-as-types correspondence to re-write the types as propositions:
证明树,逻辑地。让我们重写每个证明树以消除程序,只留下类型。同时,我们使用命题作为类型的对应关系将类型重写为命题:

--------
 p |- p


   env, p1 |- p2
-------------------
  env |- p1 -> p2


env |- p1 -> p2     env |- p1
-----------------------------
        env |- p2


env |- p1      env |- p2
------------------------
    env |- p1 /\ p2


env |- p1 /\ p2
---------------
   env |- p1


env |- p1 /\ p2
---------------
   env |- p2

Each rule can now be read as a valid form of logical reasoning. Whenever we write env |- t, it means that “from the assumptions in env, we can conclude p holds”. A rule, as usual, means that from all the premisses above the line, the conclusion below the line holds.
现在,每条规则都可以被解读为逻辑推理的有效形式。每当我们写 env |- t 时,就意味着“根据 env 中的假设,我们可以得出 p 成立的结论”。像往常一样,规则意味着根据线上方的所有前提,线下方的结论成立。

Proofs and programs. Now consider the following proof tree, showing the derivation of the type of a program:
证明和程序。现在考虑下面的证明树,显示了程序类型的推导:

------------------------           ------------------------
{p : a * b} |- p : a * b           {p : a * b} |- p : a * b
------------------------           ------------------------
{p : a * b} |- snd p : b           {p : a * b} |- fst p : a
-----------------------------------------------------------
        {p : a * b} |- (snd p, fst p) : b * a
     ----------------------------------------------
     {} |- fun p -> (snd p, fst p) : a * b -> b * a

That program shows that you can swap the components of a pair, thus swapping the types involved.
该程序表明您可以交换一对的组件,从而交换所涉及的类型。

If we erase the program, leaving only the types, and re-write those as propositions, we get this proof tree:
如果我们删除程序,只留下类型,并将它们重写为命题,我们就会得到这个证明树:

----------------           ----------------
a /\ b |- a /\ b           a /\ b |- a /\ b
----------------          -----------------
  a /\ b |- b                a /\ b |- a
--------------------------------------------
              a /\ b |- b /\ a
          ------------------------
           {} |- a /\ b -> b /\ a

And that is a valid proof tree for propositional logic. It shows that you can swap the sides of a conjunction.
这是命题逻辑的有效证明树。它表明你可以交换连词的两边。

What we see from those two proof trees is: a program is a proof. A well-typed program corresponds to the proof of a logical proposition. It shows how to compute with evidence, in this case transforming a proof of a /\ b into a proof of b /\ a.
从这两个证明树中我们看到的是:一个程序就是一个证明。一个类型良好的程序对应于一个逻辑命题的证明。它展示了如何使用证据进行计算,在本例中将 a /\ b 的证明转换为 b /\ a 的证明。

Programs are proofs. We have now created the following correspondence that enables us to read programs as proofs:
程序就是证据。我们现在创建了以下对应关系,使我们能够阅读程序作为证明:

  • A program e : t corresponds to a proof of the logical formula to which t itself corresponds.
    程序 e : t 对应于 t 本身对应的逻辑公式的证明。

  • The proof tree of |- t corresponds to the proof tree of {} |- e : t.
    |- t 的证明树对应于 {} |- e : t 的证明树。

  • The proof rules for typing a program correspond to the rules for proving a proposition.
    输入程序的证明规则对应于证明命题的规则。

But that is only the second level of the Curry-Howard correspondence. It goes deeper…
但这只是库里与霍华德通信的第二层。它更深入……

Evaluation Corresponds to Simplification
求值对应于简化 ¶

We will treat this part of the correspondence only briefly. Consider the following program:
我们将仅简要处理这部分信件。考虑以下程序:

fst (a, b)

That program would of course evaluate to a.
该程序当然会评估为 a

Next, consider the typing derivation of that program. The variables a and b must be bound to some types in the static environment for the program to type check.
接下来,考虑该程序的类型推导。变量 ab 必须绑定到静态环境中的某些类型,以便程序进行类型检查。

------------------------         -------------------------
{a : t, b : t'} |- a : t         {a : t, b : t'} |- b : t'
---------------------------------------------------------
         {a : t, b : t'} |- (a, b) : t * t'
         ----------------------------------
         {a : t, b : t'} |- fst (a, b) : t

Erasing that proof tree to just the propositions, per the proofs-as-programs correspondence, we get this proof tree:
根据“证明即程序”的对应关系,将证明树删除为仅命题,我们得到这个证明树:

-----------            -----------
t, t' |-  t            t, t' |- t'
----------------------------------
         t, t' |- t /\ t'
         ----------------
            t, t' |- t

However, there is a much simpler proof tree with the same conclusion:
然而,有一个更简单的证明树,具有相同的结论:

 ------------
  t, t' |- t

In other words, we don’t need the detour through proving t /\ t' to prove t, if t is already an assumption. We can instead just directly conclude t.
换句话说,如果 t 已经是一个假设,那么我们不需要通过证明 t /\ t' 来证明 t 。我们可以直接得出 t 的结论。

Likewise, there is a simpler typing derivation corresponding to that same simpler proof:
同样,有一个更简单的类型推导对应于相同的更简单的证明:

------------------------
{a : t, b : t'} |- a : t

Note that typing derivation is for the program a, which is exactly what the bigger program fst (a, b) evaluates to.
请注意,类型推导是针对程序 a 的,这正是较大程序 fst (a, b) 的计算结果。

Thus evaluation of the program causes the proof tree to simplify, and the simplified proof tree is actually (through the proofs-as programs correspondence) a simpler proof of the same proposition. Evaluation therefore corresponds to proof simplification. And that is the final level of the Curry-Howard correspondence.
因此,程序的评估导致证明树简化,并且简化的证明树实际上(通过证明程序对应)是同一命题的更简单的证明。因此,评估就对应于证明简化。这就是库里与霍华德通信的最终水平。

What It All Means
这一切意味着什么 ¶

Logic is a fundamental aspect of human inquiry. It guides us in reasoning about the world, in drawing valid inferences, in deducing what must be true vs. what must be false. Training in logic and argumentation—in various fields and disciplines—is one of the most important parts of a higher education.
逻辑是人类探究的一个基本方面。它指导我们推理世界,得出有效的推论,推断什么是正确的,什么是错误的。各个领域和学科的逻辑和论证训练是高等教育最重要的部分之一。

The Curry-Howard correspondence shows that logic and computation are fundamentally linked in a deep and maybe even mysterious way. The basic building blocks of logic (propositions, proofs) turn out to correspond to the basic building blocks of computation (types, functional programs). Computation itself, the evaluation or simplification of expressions, turns out to correspond to simplification of proofs. The very task that computers do therefore is the same task that humans do in trying to present a proof in the best possible way.
库里与霍华德的通信表明,逻辑和计算以一种深刻甚至神秘的方式从根本上联系在一起。逻辑的基本构建块(命题、证明)与计算的基本构建块(类型、函数程序)相对应。计算本身,即表达式的求值或简化,与证明的简化相对应。因此,计算机所做的任务与人类所做的任务是相同的,即试图以最佳方式提供证明。

Computation is thus intrinsically linked to reasoning. And functional programming is a fundamental part of human inquiry.
因此,计算与推理有着内在的联系。函数式编程是人类探究的基本组成部分。

Could there be a better reason to study functional programming?
还有更好的理由来学习函数式编程吗?

Exercises 练习 ¶

Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
大多数练习的解决方案都是可用的。这些解决方案将于 2022 年秋季首次公开发布。尽管它们已经向康奈尔大学的学生提供了几年,但不可避免的是,更广泛的流通将揭示可以做出的改进。我们很乐意添加或更正解决方案。请通过 GitHub 做出贡献。


Exercise: propositions as types [★★]
练习:命题作为类型[★★]

For each of the following propositions, write its corresponding type in OCaml.
对于以下每个命题,在 OCaml 中写出其相应的类型。

  • true -> p

  • p /\ (q /\ r)

  • (p \/ q) \/ r

  • false -> p


Exercise: programs as proofs [★★★]
练习:用程序作为证明[★★★]

For each of the following propositions, determine its corresponding type in OCaml, then write an OCaml let definition to give a program of that type. Your program proves that the type is inhabited, which means there is a value of that type. It also proves the proposition holds.
对于以下每个命题,确定其在 OCaml 中对应的类型,然后编写 OCaml let 定义以给出该类型的程序。您的程序证明该类型已被占用,这意味着存在该类型的值。也证明了这个命题的成立。

  • p /\ q -> q /\ p

  • p \/ q -> q \/ p


Exercise: evaluation as simplification [★★★]
练习:评估作为简化[★★★]

Consider the following OCaml program:
考虑以下 OCaml 程序:

let f x = snd ((fun x -> x, x) (fst x))
  • What is the type of that program?
    该程序的类型是什么?

  • What is the proposition corresponding to that type?
    该类型对应的命题是什么?

  • How would f (1,2) evaluate in the small-step semantics?
    f (1,2) 在小步语义中如何评估?

  • What simplified implementation of f does that evaluation suggest? (or perhaps there are several, though one is probably the simplest?)
    该评估建议对 f 进行怎样的简化实现? (或者也许有几个,尽管其中一个可能是最简单的?)

  • Does your simplified f still have the same type as the original? (It should.)
    您的简化 f 仍然具有与原始类型相同的类型吗? (它应该。)

Your simplified f and the original f are both proofs of the same proposition, but evaluation has helped you to produce a simpler proof.
您的简化 f 和原始 f 都是同一命题的证明,但评估已帮助您生成更简单的证明。

Big-Oh Notation 大哦表示法 ¶

What does it mean to be efficient? Cornell professors Jon Kleinberg and Eva Tardos have a wonderful explanation in chapter 2 of their textbook, Algorithm Design (2006). This appendix is a summary and reinterpretation of that explanation from a functional programming perspective. The ultimate answer will be that an algorithm is efficient if its worst-case running time on input size n is O(nd) for some constant d. But it will take us several steps to build up to that definition.
高效意味着什么?康奈尔大学教授 Jon Kleinberg 和 Eva Tardos 在他们的教科书《算法设计》(2006 年)的第 2 章中给出了精彩的解释。本附录是从函数式编程角度对该解释的总结和重新解释。最终的答案是,如果对于某个常量 d ,在输入大小 n 上的最坏情况运行时间为 O(nd) ,则该算法是有效的。但我们需要几个步骤来建立这个定义。

Algorithms and Efficiency, Attempt 1
算法和效率,尝试 1 ¶

A naive attempt at defining efficiency might proceed as follows:
定义效率的天真的尝试可能会如下进行:

Attempt 1: An algorithm is efficient if, when implemented, it runs in a small amount of time on particular input instances.
尝试 1:如果算法在实现时能够在特定输入实例上运行很短的时间,那么该算法就是高效的。

But there are many problems with that definition, such as:
但这个定义存在很多问题,例如:

  • Inefficient algorithms can run quickly on small test cases.
    低效的算法可以在小型测试用例上快速运行。

  • Fast processors and optimizing compilers can make inefficient algorithms run quickly.
    快速处理器和优化编译器可以使低效算法快速运行。

  • Efficient algorithms can run slowly when coded sloppily.
    当编码马虎时,高效的算法可能会运行缓慢。

  • Some input instances are harder than others.
    某些输入实例比其他实例更难。

  • Efficiency on small inputs doesn’t imply efficiency on large inputs.
    小投入的效率并不意味着大投入的效率。

  • Some clients can afford to be more patient than others; quick for me might be slow for you.
    有些客户可以比其他客户更有耐心;有些客户可以比其他客户更有耐心。对我来说快,对你来说可能慢。

Lesson 1: One lesson learned from that attempt is: time measured by a clock is not the right metric for algorithm efficiency. We need a metric that is reasonably independent of the hardware, compiler, other software that is running, etc. Perhaps a good metric would be to give up on time and instead count the number of steps taken during evaluation.
教训 1:从这次尝试中吸取的一个教训是:时钟测量的时间并不是衡量算法效率的正确指标。我们需要一个相当独立于硬件、编译器、正在运行的其他软件等的指标。也许一个好的指标是按时放弃,而是计算评估期间采取的步骤数。

But, now we have a new problem: how should we define a “step”? It needs to be something machine independent. It ought to somehow represent a primitive unit of computation. There’s a lot of flexibility. Here are some common choices:
但是,现在我们遇到了一个新问题:我们应该如何定义“步骤”?它需要是独立于机器的东西。它应该以某种方式代表一个原始的计算单位。有很大的灵活性。以下是一些常见的选择:

  • In pseudocode, we might think of a step as being a single line.
    在伪代码中,我们可能将步骤视为一行。

  • In imperative languages, we might count assignments, array indexes, pointer dereferences, and arithmetic operations as steps.
    在命令式语言中,我们可以将赋值、数组索引、指针取消引用和算术运算算作步骤。

  • In OCaml, we could count function or operator application, let bindings, and choosing a branch of an if or match as steps.
    在 OCaml 中,我们可以计算函数或运算符应用程序、let 绑定以及选择 ifmatch 的分支作为步骤。

In reality, all of those “steps” could really take a different amount of time. But in practice, that doesn’t really matter much.
事实上,所有这些“步骤”可能确实需要不同的时间。但实际上,这并不重要。

Lesson 2: Another lesson we learned from attempt 1 was: running time on a particular input instance is not the right metric. We need a metric that can predict running time on any input instance. So instead of using the particular input (e.g., a number, or a matrix, or a text document), it might be better to use the size of the input (e.g., the number of bits it takes to represent the number, or the number of rows and columns in the matrix, or the number of bytes in the document) as the metric.
教训 2:我们从尝试 1 中学到的另一个教训是:特定输入实例上的运行时间不是正确的指标。我们需要一个可以预测任何输入实例上的运行时间的指标。因此,与其使用特定的输入(例如,数字、矩阵或文本文档),不如使用输入的大小(例如,表示数字所需的位数,或矩阵中的行数和列数,或文档中的字节数)作为度量。

But again we have a new problem: how to define “size”? As in the examples we just gave, size should be some measure of how big an input is compared to other inputs. Perhaps the most common representation of size in the context of data structures is just the number of elements maintained by the data structure: the number of nodes in a list, or the number of nodes and edges in a graph, etc.
但我们又遇到了一个新问题:如何定义“尺寸”?正如我们刚刚给出的示例一样,大小应该是衡量输入与其他输入相比有多大的度量。也许在数据结构上下文中,大小最常见的表示只是数据结构维护的元素数量:列表中的节点数量,或图中的节点和边的数量等。

Could an algorithm run in a different amount of time on two inputs of the same “size”? Sure. For example, multiplying a matrix by all zeroes might be faster than multiplying by arbitrary numbers. But in practice, size matters more than exact inputs.
算法能否在相同“大小”的两个输入上运行不同的时间?当然。例如,将矩阵乘以全零可能比乘以任意数字更快。但在实践中,规模比精确的输入更重要。

Lesson 3: A third lesson we learned from attempt 1 was that “small amount of time” is too relative a term. We want a metric that is reasonably objective, rather than relying on subjective notions of what constitutes “small”.
教训 3:我们从尝试 1 中学到的第三个教训是“少量时间”这个词太相对了。我们想要一个相当客观的衡量标准,而不是依赖于“小”​​的主观概念。

One sort-of-okay idea would be that an efficient algorithm needs to beat brute-force search. That means enumerating all answers one-by-one, checking each to see whether it’s right. For example, a brute-force sorting algorithm would enumerate every possible permutation of a list, checking to see whether it’s a sorted version of the input list. That’s a terrible sorting algorithm! Certainly quicksort beats it.
一种不错的想法是,一种有效的算法需要击败暴力搜索。这意味着一一列举所有答案,检查每个答案是否正确。例如,强力排序算法会枚举列表的每个可能的排列,检查它是否是输入列表的排序版本。这是一个糟糕的排序算法!当然,快速排序胜过它。

Brute-force search is the simple, dumb solution to nearly any algorithmic problem. But it requires enumeration of a huge space. In fact, an exponentially-sized space. So a better idea would be doing less than exponential work. What’s less than exponential (e.g., 2n)? One possibility is polynomial (e.g., n2).
暴力搜索是几乎所有算法问题的简单、愚蠢的解决方案。但它需要枚举一个巨大的空间。事实上,这是一个指数级大小的空间。因此,更好的想法是做少于指数级的工作。什么小于指数(例如 2n )?一种可能性是多项式(例如, n2 )。

An immediate objection might be that polynomials come in all sizes. For example, n100 is way bigger than n2. And some non-polynomials, such as n1+.02(logn), might do an adequate job of beating exponentials. But in practice, polynomials do seem to work fine.
一个直接的反对意见可能是多项式有各种大小。例如, n100n2 大得多。一些非多项式,例如 n1+.02(logn) ,可能足以击败指数。但在实践中,多项式似乎确实工作得很好。

Algorithms and Efficiency, Attempt 2
算法和效率,尝试 2 ¶

Combining lessons 1 through 3 from Attempt 1, we have a second attempt at defining efficiency:
结合尝试 1 中的经验教训 1 到 3,我们对定义效率进行了第二次尝试:

Attempt 2: An algorithm is efficient if its maximum number of execution steps is polynomial in the size of its input.
尝试 2:如果算法的最大执行步骤数是其输入大小的多项式,则该算法是有效的。

Note how all three ideas come together there: steps, size, polynomial.
注意这三个想法是如何结合在一起的:步长、大小、多项式。

But if we try to put that definition to use, it still isn’t perfect. Coming up with an exact formula for the maximum number of execution steps can be insanely tedious. For example, in one other algorithm textbook, the authors develop this following polynomial for the number of execution steps taken by a pseudo-code implementation of insertion sort:
但如果我们尝试使用这个定义,它仍然不完美。得出最大执行步骤数的精确公式可能非常乏味。例如,在另一本算法教科书中,作者为插入排序的伪代码实现所采取的执行步骤数开发了以下多项式:

c1n+c2(n1)+c4(n1)+c5j=2ntj+c6j=2n(tj1)+c7j=2n(tj1)+c8(n1)

No need for us to explain what all the variables mean. It’s too complicated. Our hearts go out to the poor grad student who had to work out that one!
我们不需要解释所有变量的含义。它太复杂了。我们的心与那位必须解决这个问题的可怜的研究生同在!

Note 笔记

That formula for running time of insertion sort is from Introduction to Algorithms, 3rd edition, 2009, by Cormen, Leiserson, Rivest, and Stein. We aren’t making fun of them. They would also tell you that such formulas are too complicated.
插入排序运行时间的公式来自于 Cormen、Leiserson、Rivest 和 Stein 于 2009 年第 3 版的《算法导论》。我们不是在取笑他们。他们还会告诉你这样的公式太复杂了。

Precise execution bounds like that are exhausting to find and somewhat meaningless. If it takes 25 steps in Java pseudocode, but compiled down to RISC-V would take 250 steps, is the precision useful?
像这样精确的执行界限很难找到,而且有些毫无意义。如果 Java 伪代码需要 25 步,但编译成 RISC-V 需要 250 步,那么精度有用吗?

In some cases, yes. If you’re building code that flies an airplane or controls a nuclear reactor, you might actually care about precise, real-time guarantees.
在某些情况下,是的。如果您正在构建驾驶飞机或控制核反应堆的代码,您可能实际上关心精确、实时的保证。

But otherwise, it would be better for us to identify broad classes of algorithms with similar performance. Instead of saying that an algorithm runs in
但除此之外,我们最好能识别出具有相似性能的广泛类别的算法。而不是说算法运行在

1.62n2+3.5n+8

steps, how about just saying it runs in n2 steps? That is, we could ignore the low-order terms and the constant factor of the highest-order term.
步骤,只说它以 n2 步骤运行怎么样?也就是说,我们可以忽略低阶项和最高阶项的常数因子。

We ignore low-order terms because we want to THINK BIG. Algorithm efficiency is all about explaining the performance of algorithms when inputs get really big. We don’t care so much about small inputs. And low-order terms don’t matter when we think big. The following table shows the number of steps as a function of input size N, assuming each step takes 1 microsecond. “Very long” means more than the estimated number of atoms in the universe.
我们忽略低阶项,因为我们想要大局观。算法效率就是解释当输入变得非常大时算法的性能。我们不太关心小的投入。当我们胸怀大志时,低阶项并不重要。下表显示了作为输入大小 N 的函数的步数,假设每个步需要 1 微秒。 “非常长”意味着超过宇宙中原子的估计数量。

N

N2

N3

2N

N=10 N=10

< 1 sec < 1 秒

< 1 sec < 1 秒

< 1 sec < 1 秒

N=100 N=100

< 1 sec < 1 秒

< 1 sec < 1 秒

1 sec 1秒

N=1,000 N=1,000

< 1 sec < 1 秒

1 sec 1秒

18 min 18分钟

N=10,000 N=10,000

< 1 sec < 1 秒

2 min 2分钟

12 days 12天

N=100,000 N=100,000

< 1 sec < 1 秒

3 hours 3小时

32 years 32岁

N=1,000,000 N=1,000,000

1 sec 1秒

12 days 12天

104 years 104 年

As you can see, when inputs get big, there’s a serious difference between each column of the table. We might as well ignore low-order terms, because they are completely dominated by the highest-order term when we think big.
正如您所看到的,当输入变大时,表的每一列之间都会存在严重差异。我们不妨忽略低阶项,因为当我们进行大思考时,它们完全被最高阶项所支配。

What about constant factors? My current laptop might be 2x faster (that is, a constant factor of 2) than the one I bought several years ago, but that’s not an interesting property of the algorithm. Likewise, 1.62n2 steps in pseduocode might be 1620n2 steps in assembly (that is, a constant factor of 1000), but it’s again not an interesting property of the algorithm. So, should we really care if one algorithm takes 2x or 1000x longer than another, if it’s just a constant factor?
常数因子呢?我现在的笔记本电脑可能比我几年前购买的笔记本电脑快 2 倍(即常数因子 2),但这并不是算法的一个有趣特性。同样,伪代码中的 1.62n2 步骤可能是汇编中的 1620n2 步骤(即常数因子 1000),但这又不是算法的一个有趣属性。那么,如果一种算法只是一个常数因子,我们真的应该关心一种算法是否比另一种算法花费 2 倍或 1000 倍的时间吗?

The answer is: maybe. Performance tuning in real-world code is about getting the constants to be small. Your employer might be really happy if you make something run twice as fast! But that’s not about the algorithm. When we’re measuring algorithm efficiency, in practice the constant factors just don’t matter much.
答案是:也许吧。实际代码中的性能调优就是让常量变小。如果您让某些东西的运行速度提高两倍,您的雇主可能会非常高兴!但这与算法无关。当我们测量算法效率时,实际上常数因素并不重要。

So all that argues for having an imprecise abstraction to measure running time. Instead of 1.62n2+3.5n+8, we can just write n2. Imprecise abstractions are nothing new to you. You might write ±1 to imprecisely abstract a quantity within 1. In computer science, you already know that we use Big-Oh notation as an imprecise abstraction: 1.62n2+3.5n+8 is O(n2).
因此,所有这些都主张采用不精确的抽象来衡量运行时间。我们可以只写 n2 而不是 1.62n2+3.5n+8 。不精确的抽象对你来说并不新鲜。您可以编写 ±1 来不精确地抽象 1 以内的量。在计算机科学中,您已经知道我们使用 Big-Oh 表示法作为不精确的抽象: 1.62n2+3.5n+8O(n2)

Big-Ell Notation Big-Ell 表示法 ¶

Before reviewing Big-Oh notation, let’s start with something simpler that you might not have seen before: Big-Ell notation.
在回顾 Big-Oh 表示法之前,让我们先从一些您以前可能没有见过的更简单的表示法开始:Big-Ell 表示法。

Big-Ell is an imprecise abstraction of natural numbers less than or equal to another number, hence the L. It’s defined as follows:
Big-Ell 是对大于或等于另一个数的自然数的不精确抽象,因此是 L。它的定义如下:

L(n)={m|0mn}

where m and n are natural numbers. That is, L(n) represents all the natural numbers less than or equal to n. For example, L(5)={0,1,2,3,4,5}.
其中 mn 是自然数。即 L(n) 表示所有小于或等于 n 的自然数。例如, L(5)={0,1,2,3,4,5}

Could you do arithmetic with Big-Ell? For example, what would 1+L(5) be? It’s not a well-posed question, to be honest: addition is an operation we think of being defined on integers, not sets of integers. But a reasonable interpretation of 1+{0,1,2,3,4,5} could be doing the addition for each element in the set, yielding {1,2,3,4,5,6}. Note that {1,2,3,4,5,6} is a proper subset of {0,1,2,3,4,5,6}, and the latter is L(6). So we could say that 1+L(5)L(6). We could even say that 1+L(5)L(7), but it’s not tight: the former subset relation included the fewest possible extra elements, whereas the latter was loose by needlessly including extra.
你能用 Big-Ell 做算术吗?例如, 1+L(5) 是什么?老实说,这不是一个适定的问题:我们认为加法是一种在整数上定义的运算,而不是在整数集上定义的运算。但对 1+{0,1,2,3,4,5} 的合理解释可能是对集合中的每个元素进行加法,产生 {1,2,3,4,5,6} 。请注意, {1,2,3,4,5,6}{0,1,2,3,4,5,6} 的真子集,后者是 L(6) 。所以我们可以说 1+L(5)L(6) 。我们甚至可以说 1+L(5)L(7) ,但它并不严格:前一个子集关系包含尽可能少的额外元素,而后者由于不必要地包含额外元素而变得松散。

For more about Big Ell, see Concrete Mathematics, chapter 9, 1989, by Graham, Knuth, and Patashnik.
有关 Big Ell 的更多信息,请参阅 Concrete Mathematics,第 9 章,1989 年,作者:Graham、Knuth 和 Patashnik。

Big-Oh Notation 大哦表示法 ¶

If you understand Big-Ell, and you understand functional programming, here’s some good news: you can easily understand Big-Oh.
如果您了解 Big-Ell,并且了解函数式编程,那么这里有一些好消息:您可以轻松了解 Big-Oh。

Let’s build up the definition of Big-Oh in a few steps. We’ll start with version 1, which we’ll write as O1. It’s based on L:
让我们通过几个步骤来构建 Big-Oh 的定义。我们将从版本 1 开始,将其写为 O1 。它基于 L

  • L(n) represents any natural number that is less than or equal to a natural number n.
    L(n) 表示小于或等于自然数 n 的任何自然数。

  • O1(g) represents any natural function that is less than or equal to a natural function g.
    O1(g) 表示小于或等于自然函数 g 的任何自然函数。

A natural function is just a function on natural numbers; that is, its type is NN.
自然函数只是自然数的函数;也就是说,它的类型是 NN

All we do with O1 is upgrade from natural numbers to natural functions. So Big-Oh version 1 is just the higher-order version of Big-Ell. How about that!
我们对 O1 所做的就是从自然数升级到自然函数。所以 Big-Oh 版本 1 只是 Big-Ell 的高阶版本。那个怎么样!

Of course, we need to work out what it means for a function to be less than another function. Here’s a reasonable formalization:
当然,我们需要弄清楚一个函数小于另一个函数意味着什么。这是一个合理的形式化:

Big-Oh Version 1: O1(g)={f|n.f(n)g(n)}
Big-Oh 版本 1: O1(g)={f|n.f(n)g(n)}

For example, consider the function that doubles its input. In math textbooks, that function might be written as g(n)=2n. In OCaml we would write let g n = 2 * n or let g = fun n -> 2 * n or just anonymously as fun n -> 2 * n. In math that same anonymous function would be written with lambda notation as λn.2n. Proceeding with lambda notation, we have:
例如,考虑将其输入加倍的函数。在数学教科书中,该函数可能会写为 g(n)=2n 。在 OCaml 中,我们会编写 let g n = 2 * nlet g = fun n -> 2 * n 或只是匿名地编写为 fun n -> 2 * n 。在数学中,相同的匿名函数将使用 lambda 表示法编写为 λn.2n 。继续使用 lambda 表示法,我们有:

O1(λn.2n)={f|n.f(n)2n}

and therefore 因此

  • (λn.n)O1(λn.2n),  (λn.n)O1(λn.2n)

  • (λn.n2)O1(λn.2n), but  (λn.n2)O1(λn.2n) ,但是

  • (λn.3n)O1(λn.2n).

Next, recall that in defining algorithmic efficiency, we wanted to ignore constant factors. O1 does not help us with that. We’d really like for all these functions:
接下来,回想一下,在定义算法效率时,我们想忽略常数因素。 O1 对此没有帮助。我们真的很想要所有这些功能:

  • (λn.n)

  • (λn.2n)

  • (λn.3n)

to be in O(λn.n).
位于 O(λn.n) 中。

Toward that end, let’s define O2 to ignore constant factors:
为此,我们定义 O2 来忽略常量因素:

Big-Oh Version 2: O2(g)={f|c>0n.f(n)cg(n)}
Big-Oh 版本 2: O2(g)={f|c>0n.f(n)cg(n)}

That existentially-quantified positive constant c lets us “bump up” the function g to whatever constant factor we need. For example,
存在量化的正常数 c 让我们可以将函数 g “提升”到我们需要的任何常数因子。例如,

O2(λn.n3)={f|c>0n.f(n)cn3}

and therefore (λn.3n3)O2(λn.n3), because 3n3cn3 if we take c=3, or c=4, or any larger c.
因此 (λn.3n3)O2(λn.n3) ,因为 3n3cn3 如果我们采用 c=3 ,或 c=4 ,或任何更大的 c

Finally, recall that we don’t care about small inputs: we want to THINK BIG when we analyze algorithmic efficiency. It doesn’t matter whether the running time of an algorithm happens to be a little faster or a little slower for small inputs. In fact, we could just hardcode a lookup table for those small inputs if the algorithm is too slow on them! What matters really is the performance on big-sized inputs.
最后,请记住,我们不关心小输入:当我们分析算法效率时,我们希望从大处着眼。对于小输入,算法的运行时间是否快一点或慢一点并不重要。事实上,如果算法对这些小输入太慢,我们可以为这些小输入硬编码一个查找表!真正重要的是大尺寸输入的性能。

Toward that end, let’s define O3 to ignore small inputs:
为此,我们定义 O3 来忽略小输入:

Big-Oh Version 3: O3(g)={f|c>0n0>0nn0.f(n)cg(n)}
Big-Oh 版本 3: O3(g)={f|c>0n0>0nn0.f(n)cg(n)}

That existentially quantified positive constant n0 lets us “ignore” all inputs of that size or smaller. For example,
存在量化的正常数 n0 让我们“忽略”该大小或更小的所有输入。例如,

O3(λn.n2)={f|c>0n0>0nn0.f(n)cn2}

and therefore (λn.2n)O3(λn.n2), because 2ncn2 if we take c=2 and n0=2. Note how we get to ignore the fact that λn.2n is temporarily a little too big at n=1 by picking n0=2. That’s the power of ignoring “small” inputs.
因此 (λn.2n)O3(λn.n2) ,因为 2ncn2 如果我们采用 c=2n0=2 。请注意,我们如何通过选择 n0=2 来忽略 λn.2nn=1 处暂时太大的事实。这就是忽略“小”输入的力量。

Big-Oh, Finished 大哦,完成了 ¶

Version 3 is the right definition of Big-Oh. We repeat it here, for real:
第 3 版是 Big-Oh 的正确定义。我们在这里重复一遍,真的:

Big-Oh: O(g)={f|c>0n0>0nn0.f(n)cg(n)} 大哦: O(g)={f|c>0n0>0nn0.f(n)cg(n)}

That’s the final, important version you should know. But don’t just memorize it. If you understand the derivation we gave here, you’ll be able to recreate it from scratch anytime you need it.
这是您应该了解的最终重要版本。但不要只是记住它。如果您理解我们在这里给出的推导,您将能够在需要时随时从头开始重新创建它。

Big-Oh is called an asymptotic upper bound. If fO(g), then f is at least as efficient as g, and might be more efficient.
Big-Oh 称为渐近上限。如果 fO(g) ,则 f 至少与 g 一样有效,并且可能更有效。

Big-Oh Notation Warnings
Big-Oh 符号警告 ¶

Warning 1. Because it’s an upper bound, we can always inflate a Big-Oh statement: for example, if fO(n2), then also fO(n3), and fO(2n), etc. But our goal is always to give tight upper bounds, whether we explicitly say that or not. So when asked what the running time of an algorithm is, you must always give the tightest bound you can with Big-Oh.
警告 1。因为它是一个上限,所以我们总是可以夸大 Big-Oh 语句:例如, if fO(n2) ,则 fO(n3)fO(2n) ,但我们的目标始终是给出严格的上限,无论我们是否明确这么说。因此,当被问及算法的运行时间是多少时,您必须始终给出 Big-Oh 所能给出的最严格的限制。

Warning 2. Instead of O(g)={f|}, most authors instead write O(g(n))={f(n)|}. They don’t really mean g applied to n. They mean a function g parameterized on input n but not yet applied. This is badly misleading and generally a result of not understanding anonymous functions. Moral of that story: more people need to study functional programming.
警告 2。大多数作者不写 O(g)={f|} ,而是写 O(g(n))={f(n)|} 。它们实际上并不意味着 g 应用于 n 。它们意味着在输入 n 上参数化的函数 g 但尚未应用。这是严重的误导,通常是不理解匿名函数的结果。这个故事的寓意是:更多的人需要学习函数式编程。

Warning 3. Instead of λn.2nO(λn.n2) nearly all authors write 2n=O(n2). This is a hideous and inexcusable abuse of notation that should never have been allowed and yet has permanently infected the computer science consciousness. The standard defense is that = here should be read as “is” not as “equals”. That is patently ridiculous, and even those who make that defense usually have the good grace to admit it’s nonsense. Sometimes we become stuck with the mistakes of our ancestors. This is one of those times. Be careful of this “one-directional equality” and, if you ever have a chance, teach your (intellectual) children to do better.
警告 3。几乎所有作者都写 2n=O(n2) 而不是 λn.2nO(λn.n2) 。这是一种可怕且不可原谅的符号滥用,本来就不应该被允许,但却永久地感染了计算机科学意识。标准的辩护是这里的 = 应该被理解为“是”而不是“等于”。这显然是荒谬的,甚至那些做出这种辩护的人通常也很优雅地承认这是无稽之谈。有时我们会陷入祖先的错误之中。这是其中之一。小心这种“单向平等”,如果有机会,请教你的(智力)孩子做得更好。

Algorithms and Efficiency, Attempt 3
算法和效率,尝试 3 ¶

Let’s review. Our first attempt at defining efficiency was:
我们来复习。我们定义效率的第一次尝试是:

Attempt 1: An algorithm is efficient if, when implemented, it runs in a small amount of time on particular input instances.
尝试 1:如果算法在实现时能够在特定输入实例上运行很短的时间,那么该算法就是高效的。

By replacing time with steps, particular instances with input size, and small with polynomial, we improved that to:
通过用步长替换时间、用输入大小替换特定实例、用多项式替换小,我们将其改进为:

Attempt 2: An algorithm is efficient if its maximum number of execution steps is polynomial in the size of its input.
尝试 2:如果算法的最大执行步骤数是其输入大小的多项式,则该算法是有效的。

And that’s really a pretty good definition. But using Big-Oh notation to make it a little more concrete, we can produce our third and final attempt:
这确实是一个非常好的定义。但是使用 Big-Oh 表示法使其更具体一点,我们可以进行第三次也是最后一次尝试:

Attempt 3: An algorithm is efficient if its worst-case running time on input size n is O(nd) for some constant d.
尝试 3:如果对于某个常量 d ,其在输入大小 n 上的最坏情况运行时间为 O(nd) ,则该算法是有效的。

By “worst-case running time” we mean the same thing as “maximum number of execution steps”, just expressed in different and probably more common words. The worst-case is when execution takes the longest. “Time” is a common euphemism here for execution steps, and is used to emphasize we’re thinking about how long a computation takes.
我们所说的“最坏情况运行时间”与“最大执行步骤数”的意思相同,只是用不同且可能更常见的词语来表达。最坏的情况是执行时间最长。 “时间”是执行步骤的常见委婉说法,用于强调我们正在考虑计算需要多长时间。

Space is the most common other feature of efficiency to consider. Algorithms can be more or less efficient at requiring constant or linear space, for example. You’re already familiar with that from tail recursion and lists in OCaml.
空间是最常见的要考虑的效率特征。例如,算法在需要恒定或线性空间时可能或多或少有效。您已经通过 OCaml 中的尾递归和列表熟悉了这一点。

Virtual Machine 虚拟机 ¶

A virtual machine is what the name suggests: a machine running virtually inside another machine. With virtual machines, there are two operating systems involved: the host operating system (OS) and the guest OS. The host is your own native OS (maybe Windows). The guest is the OS that runs inside the host.
虚拟机顾名思义:虚拟地在另一台机器内运行的机器。对于虚拟机,涉及两个操作系统:主机操作系统 (OS) 和来宾操作系统。主机是您自己的本机操作系统(可能是 Windows)。来宾是在主机内部运行的操作系统。

The virtual machine (VM) we provide here has OCaml pre-installed in an Ubuntu guest OS. Ubuntu is a free Linux OS, and is an ancient African word meaning “humanity to others”. The process we use to create the VM is documented here.
我们在此提供的虚拟机 (VM) 在 Ubuntu 客户操作系统中预装了 OCaml。 Ubuntu 是一个免费的 Linux 操作系统,是一个古老的非洲单词,意思是“对他人的人性”。我们用于创建虚拟机的过程记录在此处。

Installing the VM 安装虚拟机 ¶

  • Download and install Oracle’s free VirtualBox for your host OS. Or, if you already had it installed, make sure you update to the latest version of VirtualBox before proceeding.
    下载并安装适用于您的主机操作系统的 Oracle 免费 VirtualBox。或者,如果您已经安装了 VirtualBox,请确保在继续之前更新到 VirtualBox 的最新版本。

  • Download our VM. Don’t worry about the “We’re sorry, the preview didn’t load” message you see. Just click the Download button and save the .ova file wherever you like. It’s about a 6GB file, so the download might take awhile.
    下载我们的虚拟机。不必担心您看到的“抱歉,预览未加载”消息。只需单击“下载”按钮并将 .ova 文件保存在您喜欢的位置即可。文件大小约为 6GB,因此下载可能需要一段时间。

  • Launch VirtualBox, select File → Import Appliance, and choose the .ova file you just downloaded. Click Next, then Import.
    启动 VirtualBox,选择“文件”→“导入设备”,然后选择刚刚下载的 .ova 文件。单击“下一步”,然后单击“导入”。

Starting the VM 启动虚拟机 ¶

  • Select cs3110-2022fa-ubuntu from the list of machines in VirtualBox. Click Start. At this point various errors can occur that depend on your hardware, hence are hard to predict.
    从 VirtualBox 中的计算机列表中选择 cs3110-2022fa-ubuntu。单击开始。此时可能会发生各种错误,具体取决于您的硬件,因此很难预测。

    • If you get an error about “VT-x/AMD-V hardware acceleration”, you most likely need to access your computer’s BIOS settings and enable virtualization. The details of that will vary depending on the model and manufacturer of your computer. Try googling “enable virtualization [manufacturer] [model]”, substituting for the manufacturer and model of your machine. This Red Hat Linux page might also help.
      如果您收到有关“VT-x/AMD-V 硬件加速”的错误,您很可能需要访问计算机的 BIOS 设置并启用虚拟化。其详细信息将根据您的计算机的型号和制造商而有所不同。尝试在谷歌上搜索“启用虚拟化[制造商][型号]”,替换您机器的制造商和型号。此 Red Hat Linux 页面也可能有所帮助。

    • If you get an error about “kernel panic” and “attempted to kill the idle task”, then you might need to increase the number of processors provided to it by your host OS. Select the VM in Virtual Box, click Settings, and look at the System → Processor settings. Increase the number of processors from 1 to 2. If the sliders are greyed out and won’t permit adjustment, it means the VM is still running: you can’t change the amount of memory while the guest OS is active; so, shut down the VM (see below) and try again.
      如果您收到有关“内核恐慌”和“试图终止空闲任务”的错误,那么您可能需要增加主机操作系统提供给它的处理器数量。在 Virtual Box 中选择虚拟机,单击“设置”,然后查看“系统”→“处理器设置”。将处理器数量从 1 个增加到 2 个。如果滑块呈灰色且不允许调整,则意味着 VM 仍在运行:当来宾操作系统处于活动状态时,您无法更改内存量;因此,关闭虚拟机(见下文)并重试。

    • If the machine just freezes or blacks out or aborts, you might need to adjust the memory provided to it by your host OS. Select the VM in Virtual Box, click Settings, and look at the System and Display settings. You might need to adjust the Base Memory (under System → Motherboard) or the Video Memory (under Display → Screen). Those sliders have color coding underneath them to indicate what good amounts might be on your computer. Make sure nothing is in the red zone, and try some lower or higher settings to see if they help. If the sliders are greyed out and won’t permit adjustment, it means the VM is still running: you can’t change the amount of memory while the guest OS is active; so, shut down the VM (see below) and try again.
      如果计算机只是冻结、黑屏或中止,您可能需要调整主机操作系统提供给它的内存。在 Virtual Box 中选择虚拟机,单击“设置”,然后查看“系统”和“显示”设置。您可能需要调整基本内存(在系统 → 主板下)或视频内存(在显示 → 屏幕下)。这些滑块下面有颜色编码,以指示您的计算机上可能有多少数量。确保红色区域中没有任何内容,并尝试一些较低或较高的设置,看看是否有帮助。如果滑块呈灰色且不允许调整,则意味着虚拟机仍在运行:当客户操作系统处于活动状态时,您无法更改内存量;因此,关闭虚拟机(见下文)并重试。

    • If you have a monitor with high pixel density (e.g., an Apple Retina display), the VM window might be incredibly tiny. In VirtualBox go to Settings → Display → Scale Factor and increase it as needed, perhaps to 200%.
      如果您拥有高像素密度的显示器(例如 Apple Retina 显示屏),VM 窗口可能会非常小。在 VirtualBox 中,转到“设置”→“显示”→“比例因子”,然后根据需要增加它,也许增加到 200%。

  • The VM will log you in automatically. The username is camel and the password is camel. To change your password, run passwd from the terminal and follow the prompts. If you’d rather have your own username, you are welcome to go to Settings → Users to create a new account. Just be aware that OPAM and VS Code won’t be installed for that user. You’ll need to follow the install instructions to add them.
    虚拟机将自动让您登录。用户名是 camel ,密码是 camel 。要更改密码,请从终端运行 passwd 并按照提示操作。如果您希望拥有自己的用户名,欢迎您前往“设置”→“用户”创建一个新帐户。请注意,不会为该用户安装 OPAM 和 VS Code。您需要按照安装说明来添加它们。

Stopping the VM 停止虚拟机 ¶

You can use Ubuntu’s own menus to safely shutdown or reboot the VM. But more often you will likely use VirtualBox to close the VM by clicking the VM window’s “X” icon in the host OS. Then you will be presented with three options that VirtualBox doesn’t explain very well:
您可以使用 Ubuntu 自己的菜单来安全地关闭或重新启动虚拟机。但更常见的是,您可能会使用 VirtualBox 通过单击主机操作系统中 VM 窗口的“X”图标来关闭 VM。然后你会看到 VirtualBox 没有很好解释的三个选项:

  • Save the machine state. This option is what you normally want. It’s like closing the lid on your laptop: it puts it to sleep, and it can quickly wake.
    保存机器状态。此选项是您通常想要的。这就像合上笔记本电脑的盖子:让它进入睡眠状态,然后可以快速唤醒。

  • Send the shutdown signal. This option is like shutting down a machine you don’t intend to use for a long time, or before unplugging a desktop machine from the wall. When you start the machine again later, it will have to boot from scratch, which takes longer.
    发送关机信号。此选项就像关闭一台您长时间不打算使用的机器,或者在拔掉墙上的台式机电源之前关闭它。当您稍后再次启动机器时,它将必须从头开始启动,这需要更长的时间。

  • Power off the machine. This option is dangerous. It is the equivalent of pulling the power cord of a desktop machine from the wall while the machine is still running: it causes the operating system to suddenly quit without doing any cleanup. Doing this even just a handful of times could cause the file system to become corrupted, which will cause you to lose all your work and have to reinstall the VM from scratch. You will be very unhappy. So, avoid this option.
    关闭机器电源。这个选项很危险。这相当于在机器仍在运行时从墙上拔掉台式机的电源线:它会导致操作系统突然退出而不进行任何清理。即使只执行几次,也可能会导致文件系统损坏,从而导致您丢失所有工作,并且必须从头开始重新安装虚拟机。你会很不高兴。因此,请避免使用此选项。

Using the VM 使用虚拟机 ¶

  • There are icons provided for the terminal, VS Code, and the Firefox web browser. They are in the left-hand launcher bar.
    为终端、VS Code 和 Firefox Web 浏览器提供了图标。它们位于左侧启动器栏中。

  • It can be helpful to set up a shared folder between the host and guest OS, so that you can easily copy files between them. With the VM shutdown (i.e., select “send the shutdown signal”), click Settings, then click Shared Folders. Click the little icon on the right that looks like a folder with a plus sign. In the dialog box for Folder Path, select Other, then navigate to the folder on your host OS that you want to share with the guest OS. Let’s assume you created a new folder named vmshared inside your Documents folder, or wherever you like to keep files. The Folder Name in the dialog box will automatically be filled with vmshared. This is the name by which the guest OS will know the folder. You can change it if you like. Check Auto-mount; do not check Read-only. Make the Mount Point /home/camel/vmshared. Click OK, then click OK again. Start the VM again. You should now have a subdirectory named vmshared in your guest OS home directory that is shared between the host OS and the guest OS.
    在主机和来宾操作系统之间设置共享文件夹会很有帮助,以便您可以轻松地在它们之间复制文件。虚拟机关闭后(即选择“发送关闭信号”),单击“设置”,然后单击“共享文件夹”。单击右侧看起来像带有加号的文件夹的小图标。在“文件夹路径”对话框中,选择“其他”,然后导航到主机操作系统上要与来宾操作系统共享的文件夹。假设您在文档文件夹中或您想要保存文件的任何位置创建了一个名为 vmshared 的新文件夹。对话框中的文件夹名称将自动填充 vmshared 。这是来宾操作系统识别文件夹的名称。如果您愿意,可以更改它。勾选自动挂载;不要选中只读。设置安装点 /home/camel/vmshared 。单击“确定”,然后再次单击“确定”。再次启动虚拟机。现在,您的来宾操作系统主目录中应该有一个名为 vmshared 的子目录,该子目录在主机操作系统和来宾操作系统之间共享。

  • You might be able to improve the performance of your VM by increasing the amount of memory or CPUs allocated to it, though it depends on how much your actual machine has available and what else you have running at the same time. With the VM shut down, try going in Virtual Box to Settings → System, and tinkering with the Base Memory slider on the Motherboard tab, and the Processors slider on the Processor tab. Then bring up the VM again and see how it does. You might have to play around to find a sweet spot. Later, after you are satisfied the VM is working properly hence you won’t have to re-import it, you can safely delete the .ova file you downloaded to free up some space.
    您可以通过增加分配给虚拟机的内存或 CPU 量来提高虚拟机的性能,但这取决于您的实际计算机可用的数量以及同时运行的其他内容。 VM 关闭后,尝试进入 Virtual Box 的“设置”→“系统”,然后修改“主板”选项卡上的“基本内存”滑块和“处理器”选项卡上的“处理器”滑块。然后再次启动虚拟机,看看效果如何。您可能需要尝试一下才能找到最佳位置。稍后,当您对虚拟机正常工作感到满意后,无需重新导入它,您可以安全地删除下载的 .ova 文件以释放一些空间。